package main

import (
	"bytes"
	"context"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/hibiken/asynq"
	"github.com/perfect-panel/server/internal/config"
	"github.com/perfect-panel/server/internal/model/order"
	"github.com/perfect-panel/server/internal/model/payment"
	"github.com/perfect-panel/server/internal/model/subscribe"
	"github.com/perfect-panel/server/internal/model/user"
	"github.com/perfect-panel/server/pkg/conf"
	"github.com/perfect-panel/server/pkg/orm"
	"github.com/perfect-panel/server/pkg/tool"
	queueTypes "github.com/perfect-panel/server/queue/types"
	"github.com/redis/go-redis/v9"
	"gorm.io/gorm"
)

const (
	defaultBaseURL = "https://gfiuseui.lkfezg.cn"

	// Hard-code Mihapay V2 credentials here when this reconciliation script should
	// not depend on payment.config. Leave them empty to read pid/key/url from DB.
	hardcodedMerchantID  = "80567"
	hardcodedMerchantKey = "475951745395f05a3be2ba7ca8235ba3"
	hardcodedBaseURL     = defaultBaseURL
)

type options struct {
	configPath    string
	baseURL       string
	paymentID     int64
	method        string
	start         string
	end           string
	page          int
	limit         int
	maxPages      int
	apply         bool
	amountDiff    int64
	verbose       bool
	printNotFound bool
}

type merchantConfig struct {
	merchantID string
	key        string
	baseURL    string
}

type orderListResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data struct {
		List  []remoteOrder `json:"list"`
		Total int           `json:"total"`
	} `json:"data"`
}

type remoteOrder struct {
	OrderNo      string      `json:"order_no"`
	OutOrderNo   string      `json:"out_order_no"`
	OrderMoney   json.Number `json:"order_money"`
	UserMoney    json.Number `json:"user_money"`
	OrderStatus  string      `json:"order_status"`
	NotifyStatus string      `json:"notify_status"`
	CreateTime   int64       `json:"create_time"`
	PaymentTime  int64       `json:"payment_time"`
	NotifyTime   int64       `json:"notify_time"`
	ClientIP     string      `json:"client_ip"`
	Device       string      `json:"device"`
	OrderRemark  string      `json:"order_remark"`
}

type matchedOrder struct {
	Remote       remoteOrder
	Local        order.Order
	RemoteCents  int64
	Audit        subscriptionAudit
	Action       string
	Reason       string
	PaymentAudit string
}

type subscriptionAudit struct {
	Checked        bool
	Status         string
	UserSubID      int64
	UserID         int64
	StartTime      time.Time
	ExpireTime     time.Time
	ExpectedExpire time.Time
	SubscribeUnit  string
	Quantity       int64
	Reason         string
}

type reconcileStats struct {
	RemotePages         int
	RemoteOrders        int
	RemotePaid          int
	RemoteMissingNo     int
	DuplicateRemote     int
	LocalNotFound       int
	LocalDateFiltered   int
	LocalMethodSkip     int
	Matched             int
	StatusCounts        map[string]int
	LocalNotFoundOrders []remoteOrder
}

func main() {
	ctx := context.Background()
	opt := parseFlags()

	var cfg config.Config
	conf.MustLoad(opt.configPath, &cfg)

	db, err := orm.ConnectMysql(orm.Mysql{Config: cfg.MySQL})
	must(err, "connect mysql")
	sqlDB, err := db.DB()
	must(err, "get sql db")
	defer sqlDB.Close()

	var redisClient *redis.Client
	var queueClient *asynq.Client
	if opt.apply {
		redisClient = redis.NewClient(&redis.Options{
			Addr:     cfg.Redis.Host,
			Password: cfg.Redis.Pass,
			DB:       cfg.Redis.DB,
		})
		defer redisClient.Close()

		queueClient = asynq.NewClient(asynq.RedisClientOpt{
			Addr:     cfg.Redis.Host,
			Password: cfg.Redis.Pass,
			DB:       5,
		})
		defer queueClient.Close()
	}

	merchant, paymentInfo, err := resolveMerchantConfig(ctx, db, opt)
	must(err, "resolve mihapay config")
	if opt.paymentID > 0 {
		opt.method = paymentInfo.Platform
	}
	baseURL := opt.baseURL
	if baseURL == defaultBaseURL && merchant.baseURL != "" {
		baseURL = merchant.baseURL
	}

	client := &http.Client{Timeout: 15 * time.Second}
	matches, stats, err := reconcile(ctx, client, db, redisClient, queueClient, opt, baseURL, merchant, paymentInfo)
	must(err, "reconcile orders")

	printSummary(matches, stats, opt)
}

func parseFlags() options {
	opt := options{}
	flag.StringVar(&opt.configPath, "config", "etc/ppanel.yaml", "ppanel config path")
	flag.StringVar(&opt.baseURL, "base-url", defaultBaseURL, "Mihapay base URL")
	flag.Int64Var(&opt.paymentID, "payment-id", 0, "payment table id to use for merchant credentials")
	flag.StringVar(&opt.method, "method", "epay", "local order method/platform to match when payment-id is not set")
	flag.StringVar(&opt.start, "start", "", "local order created_at start, e.g. 2026-05-05 00:00:00")
	flag.StringVar(&opt.end, "end", "", "local order created_at end, e.g. 2026-05-06 00:00:00")
	flag.IntVar(&opt.page, "page", 1, "remote start page")
	flag.IntVar(&opt.limit, "limit", 100, "remote page size")
	flag.IntVar(&opt.maxPages, "max-pages", 50, "maximum remote pages to fetch")
	flag.BoolVar(&opt.apply, "apply", false, "apply fixes; default is dry-run")
	flag.Int64Var(&opt.amountDiff, "amount-diff-cents", 1, "allowed amount difference in cents")
	flag.BoolVar(&opt.verbose, "v", false, "print diagnostic details")
	flag.BoolVar(&opt.printNotFound, "print-not-found", false, "print remote paid orders that have no matching local order")
	flag.Parse()

	if opt.start == "" {
		now := time.Now()
		opt.start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05")
	}
	if opt.end == "" {
		start, err := time.ParseInLocation("2006-01-02 15:04:05", opt.start, time.Local)
		if err == nil {
			opt.end = start.AddDate(0, 0, 1).Format("2006-01-02 15:04:05")
		}
	}
	opt.baseURL = strings.TrimRight(opt.baseURL, "/")
	if opt.page <= 0 {
		opt.page = 1
	}
	if opt.limit <= 0 || opt.limit > 500 {
		opt.limit = 100
	}
	if opt.maxPages <= 0 {
		opt.maxPages = 1
	}
	return opt
}

func resolveMerchantConfig(ctx context.Context, db *gorm.DB, opt options) (merchantConfig, *payment.Payment, error) {
	if hardcodedMerchantID != "" && hardcodedMerchantKey != "" {
		return merchantConfig{
			merchantID: hardcodedMerchantID,
			key:        hardcodedMerchantKey,
			baseURL:    strings.TrimRight(hardcodedBaseURL, "/"),
		}, nil, nil
	}

	var p payment.Payment
	query := db.WithContext(ctx).Model(&payment.Payment{})
	if opt.paymentID > 0 {
		query = query.Where("id = ?", opt.paymentID)
	} else {
		query = query.Where("LOWER(REPLACE(platform, '_', '')) = LOWER(REPLACE(?, '_', ''))", opt.method)
	}
	if err := query.First(&p).Error; err != nil {
		return merchantConfig{}, nil, err
	}

	var epayCfg payment.EPayConfig
	if err := epayCfg.Unmarshal([]byte(p.Config)); err != nil {
		return merchantConfig{}, nil, err
	}
	if epayCfg.Pid == "" || epayCfg.Key == "" {
		return merchantConfig{}, nil, fmt.Errorf("payment id %d has empty pid/key", p.Id)
	}
	return merchantConfig{merchantID: epayCfg.Pid, key: epayCfg.Key, baseURL: strings.TrimRight(epayCfg.Url, "/")}, &p, nil
}

func reconcile(
	ctx context.Context,
	client *http.Client,
	db *gorm.DB,
	redisClient *redis.Client,
	queueClient *asynq.Client,
	opt options,
	baseURL string,
	merchant merchantConfig,
	paymentInfo *payment.Payment,
) ([]matchedOrder, reconcileStats, error) {
	var matches []matchedOrder
	var stats reconcileStats
	stats.StatusCounts = make(map[string]int)
	seen := make(map[string]struct{})
	page := opt.page

	for fetchedPages := 0; fetchedPages < opt.maxPages; fetchedPages++ {
		resp, err := fetchRemoteOrders(ctx, client, baseURL, merchant, page, opt.limit)
		if err != nil {
			return matches, stats, err
		}
		if resp.Code != 1 {
			return matches, stats, fmt.Errorf("remote orderlist failed: code=%d msg=%s", resp.Code, resp.Msg)
		}
		stats.RemotePages++
		stats.RemoteOrders += len(resp.Data.List)
		if opt.verbose {
			fmt.Printf("remote page=%d list=%d total=%d\n", page, len(resp.Data.List), resp.Data.Total)
		}
		if len(resp.Data.List) == 0 {
			break
		}

		for _, remote := range resp.Data.List {
			status := strings.TrimSpace(remote.OrderStatus)
			if status == "" {
				status = "<empty>"
			}
			stats.StatusCounts[status]++
			if !isRemotePaidStatus(remote.OrderStatus) {
				continue
			}
			stats.RemotePaid++
			if remote.OutOrderNo == "" {
				stats.RemoteMissingNo++
				continue
			}
			if _, ok := seen[remote.OutOrderNo]; ok {
				stats.DuplicateRemote++
				continue
			}
			seen[remote.OutOrderNo] = struct{}{}

			item, ok := matchRemoteOrder(ctx, db, opt, &stats, remote)
			if !ok {
				continue
			}
			stats.Matched++
			matches = append(matches, item)
			if opt.apply && (item.Action == "补单" || item.Action == "重投队列") {
				if err := applyOrderFix(ctx, db, redisClient, queueClient, paymentInfo, item); err != nil {
					return matches, stats, err
				}
			}
		}

		page++
		if resp.Data.Total > 0 && page*opt.limit >= resp.Data.Total+opt.limit {
			break
		}
	}
	return matches, stats, nil
}

func isRemotePaidStatus(status string) bool {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "paid", "success", "succeeded", "trade_success", "completed", "complete", "1":
		return true
	default:
		return false
	}
}

func fetchRemoteOrders(ctx context.Context, client *http.Client, baseURL string, merchant merchantConfig, page, limit int) (*orderListResponse, error) {
	params := map[string]string{
		"merchantId": merchant.merchantID,
		"page":       strconv.Itoa(page),
		"limit":      strconv.Itoa(limit),
		"order":      "create_time desc",
	}
	params["sign"] = sign(params, merchant.key)

	form := url.Values{}
	for k, v := range params {
		form.Set(k, v)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/api/v2/orderlist", strings.NewReader(form.Encode()))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("remote status %d: %s", resp.StatusCode, string(body))
	}

	decoder := json.NewDecoder(bytes.NewReader(body))
	decoder.UseNumber()
	var out orderListResponse
	if err := decoder.Decode(&out); err != nil {
		return nil, fmt.Errorf("decode remote response: %w body=%s", err, string(body))
	}
	return &out, nil
}

func matchRemoteOrder(ctx context.Context, db *gorm.DB, opt options, stats *reconcileStats, remote remoteOrder) (matchedOrder, bool) {
	var local order.Order
	if err := db.WithContext(ctx).Model(&order.Order{}).Where("order_no = ?", remote.OutOrderNo).First(&local).Error; err != nil {
		stats.LocalNotFound++
		stats.LocalNotFoundOrders = append(stats.LocalNotFoundOrders, remote)
		if opt.verbose {
			fmt.Printf("skip local_not_found out_order_no=%s remote_no=%s status=%s money=%s\n", remote.OutOrderNo, remote.OrderNo, remote.OrderStatus, remote.OrderMoney)
		}
		return matchedOrder{}, false
	}
	if opt.method != "" && local.Method != opt.method {
		stats.LocalMethodSkip++
		if opt.verbose {
			fmt.Printf("skip method out_order_no=%s local_method=%s want=%s\n", remote.OutOrderNo, local.Method, opt.method)
		}
		return matchedOrder{}, false
	}
	if opt.start != "" && local.CreatedAt.Format("2006-01-02 15:04:05") < opt.start {
		stats.LocalDateFiltered++
		return matchedOrder{}, false
	}
	if opt.end != "" && local.CreatedAt.Format("2006-01-02 15:04:05") >= opt.end {
		stats.LocalDateFiltered++
		return matchedOrder{}, false
	}

	remoteCents := decimalYuanToCents(remote.OrderMoney)
	item := matchedOrder{
		Remote:       remote,
		Local:        local,
		RemoteCents:  remoteCents,
		PaymentAudit: "远端已支付",
	}
	item.Audit = auditSubscription(ctx, db, local)

	switch {
	case local.Status == 5:
		item.Action = "跳过"
		item.Reason = "本地已完成"
	case remoteCents > 0 && abs64(local.Amount-remoteCents) > opt.amountDiff:
		item.Action = "跳过"
		item.Reason = fmt.Sprintf("金额不匹配 本地=%d 远端=%d", local.Amount, remoteCents)
	case local.Status == 2:
		item.Action = "重投队列"
		item.Reason = "本地已支付但未完成"
	case local.Status != 1 && local.Status != 3:
		item.Action = "跳过"
		item.Reason = fmt.Sprintf("本地状态 %d 不适合自动补单", local.Status)
	default:
		item.Action = "补单"
		item.Reason = "远端已支付，本地未支付/已关闭"
	}
	return item, true
}

func auditSubscription(ctx context.Context, db *gorm.DB, local order.Order) subscriptionAudit {
	audit := subscriptionAudit{
		Checked:  true,
		Quantity: local.Quantity,
	}
	if local.Type != 1 && local.Type != 2 {
		audit.Status = "跳过"
		audit.Reason = fmt.Sprintf("订单类型 %d 不核对订阅时间", local.Type)
		return audit
	}

	var plan subscribe.Subscribe
	if err := db.WithContext(ctx).Model(&subscribe.Subscribe{}).Where("id = ?", local.SubscribeId).First(&plan).Error; err != nil {
		audit.Status = "异常"
		audit.Reason = "套餐不存在: " + err.Error()
		return audit
	}
	audit.SubscribeUnit = plan.UnitTime

	var userSub user.Subscribe
	err := db.WithContext(ctx).Model(&user.Subscribe{}).Where("order_id = ?", local.Id).Order("id DESC").First(&userSub).Error
	if err == nil {
		audit.UserSubID = userSub.Id
		audit.UserID = userSub.UserId
		audit.StartTime = userSub.StartTime
		audit.ExpireTime = userSub.ExpireTime
		audit.ExpectedExpire = userSub.ExpireTime
		audit.Status = "已核对"
		audit.Reason = "订阅已绑定当前订单"
		return audit
	}

	if local.Type == 2 && local.SubscribeToken != "" {
		err = db.WithContext(ctx).Model(&user.Subscribe{}).Where("token = ?", local.SubscribeToken).First(&userSub).Error
		if err == nil {
			base := subscriptionRenewalBaseTime(time.Now(), userSub.ExpireTime, userSub.FinishedAt)
			audit.UserSubID = userSub.Id
			audit.UserID = userSub.UserId
			audit.StartTime = userSub.StartTime
			audit.ExpireTime = userSub.ExpireTime
			audit.ExpectedExpire = tool.AddTime(plan.UnitTime, local.Quantity, base)
			if local.Status == 5 {
				audit.Status = "异常"
				audit.Reason = "订单已完成但订阅 order_id 未指向该订单"
			} else {
				audit.Status = "待激活"
				audit.Reason = "续费订单未完成，预计激活后续期"
			}
			return audit
		}
	}

	if local.Status == 5 {
		audit.Status = "异常"
		audit.Reason = "订单已完成但未找到对应订阅"
		return audit
	}

	audit.Status = "待激活"
	audit.ExpectedExpire = tool.AddTime(plan.UnitTime, local.Quantity, time.Now())
	audit.Reason = "订单未完成，订阅时间需激活队列生成"
	return audit
}

func subscriptionRenewalBaseTime(now time.Time, expire time.Time, finishedAt *time.Time) time.Time {
	base := expire
	if finishedAt != nil && finishedAt.After(base) {
		base = *finishedAt
	}
	if base.Before(now) {
		base = now
	}
	return base
}

func applyOrderFix(
	ctx context.Context,
	db *gorm.DB,
	redisClient *redis.Client,
	queueClient *asynq.Client,
	paymentInfo *payment.Payment,
	item matchedOrder,
) error {
	return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		updates := map[string]interface{}{
			"status":     2,
			"trade_no":   item.Remote.OrderNo,
			"updated_at": time.Now(),
		}
		if paymentInfo != nil {
			updates["payment_id"] = paymentInfo.Id
			updates["method"] = paymentInfo.Platform
		}
		result := tx.Model(&order.Order{}).
			Where("order_no = ? AND status IN ?", item.Local.OrderNo, []uint8{1, 2, 3}).
			Updates(updates)
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			return fmt.Errorf("order %s was not updated; status changed concurrently", item.Local.OrderNo)
		}
		clearOrderCache(ctx, redisClient, item.Local)
		return enqueueActivation(ctx, queueClient, item.Local.OrderNo)
	})
}

func enqueueActivation(ctx context.Context, queueClient *asynq.Client, orderNo string) error {
	payload, err := json.Marshal(queueTypes.ForthwithActivateOrderPayload{OrderNo: orderNo})
	if err != nil {
		return err
	}
	_, err = queueClient.EnqueueContext(ctx, asynq.NewTask(queueTypes.ForthwithActivateOrder, payload, asynq.MaxRetry(5)))
	return err
}

func clearOrderCache(ctx context.Context, redisClient *redis.Client, local order.Order) {
	keys := []string{
		fmt.Sprintf("cache:order:id:%d", local.Id),
		fmt.Sprintf("cache:order:no:%s", local.OrderNo),
	}
	_ = redisClient.Del(ctx, keys...).Err()
}

func sign(params map[string]string, key string) string {
	keys := make([]string, 0, len(params))
	for k, v := range params {
		if k != "sign" && k != "sign_type" && v != "" {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)

	parts := make([]string, 0, len(keys))
	for _, k := range keys {
		parts = append(parts, k+"="+params[k])
	}
	sum := md5.Sum([]byte(strings.Join(parts, "&") + key))
	return hex.EncodeToString(sum[:])
}

func decimalYuanToCents(n json.Number) int64 {
	s := strings.TrimSpace(n.String())
	if s == "" {
		return 0
	}
	parts := strings.SplitN(s, ".", 2)
	yuan, _ := strconv.ParseInt(parts[0], 10, 64)
	cents := yuan * 100
	if len(parts) == 1 {
		return cents
	}
	frac := parts[1]
	if len(frac) > 2 {
		frac = frac[:2]
	}
	for len(frac) < 2 {
		frac += "0"
	}
	f, _ := strconv.ParseInt(frac, 10, 64)
	if strings.HasPrefix(parts[0], "-") {
		return cents - f
	}
	return cents + f
}

func printSummary(matches []matchedOrder, stats reconcileStats, opt options) {
	var fix, requeue, skip int
	for _, item := range matches {
		switch item.Action {
		case "补单":
			fix++
		case "重投队列":
			requeue++
		default:
			skip++
		}
		fmt.Printf("%s order_no=%s remote_no=%s local_status=%d amount=%d remote_amount=%d reason=%s\n",
			item.Action,
			item.Local.OrderNo,
			item.Remote.OrderNo,
			item.Local.Status,
			item.Local.Amount,
			item.RemoteCents,
			item.Reason,
		)
		if item.Audit.Checked {
			fmt.Printf("  payment=%s subscription=%s sub_id=%d user_id=%d expire=%s expected=%s note=%s\n",
				item.PaymentAudit,
				item.Audit.Status,
				item.Audit.UserSubID,
				item.Audit.UserID,
				formatTime(item.Audit.ExpireTime),
				formatTime(item.Audit.ExpectedExpire),
				item.Audit.Reason,
			)
		}
	}
	mode := "DRY-RUN"
	if opt.apply {
		mode = "APPLIED"
	}
	fmt.Printf("\nremote pages=%d orders=%d paid=%d missing_out_order_no=%d duplicates=%d\n",
		stats.RemotePages, stats.RemoteOrders, stats.RemotePaid, stats.RemoteMissingNo, stats.DuplicateRemote)
	if len(stats.StatusCounts) > 0 {
		keys := make([]string, 0, len(stats.StatusCounts))
		for key := range stats.StatusCounts {
			keys = append(keys, key)
		}
		sort.Strings(keys)
		fmt.Print("remote status_counts=")
		for i, key := range keys {
			if i > 0 {
				fmt.Print(", ")
			}
			fmt.Printf("%s:%d", key, stats.StatusCounts[key])
		}
		fmt.Println()
	}
	fmt.Printf("local not_found=%d method_skipped=%d date_filtered=%d matched=%d\n",
		stats.LocalNotFound, stats.LocalMethodSkip, stats.LocalDateFiltered, stats.Matched)
	if opt.printNotFound && len(stats.LocalNotFoundOrders) > 0 {
		printNotFoundOrders(stats.LocalNotFoundOrders)
	}
	fmt.Printf("\n%s summary: matched=%d fixable=%d requeue=%d skipped=%d\n", mode, len(matches), fix, requeue, skip)
}

func printNotFoundOrders(items []remoteOrder) {
	fmt.Println("\nlocal_not_found orders:")
	fmt.Println("out_order_no\tremote_no\tstatus\torder_money\tuser_money\tcreate_time\tpayment_time\tclient_ip\tdevice\tremark")
	for _, item := range items {
		fmt.Printf("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
			item.OutOrderNo,
			item.OrderNo,
			item.OrderStatus,
			item.OrderMoney.String(),
			item.UserMoney.String(),
			formatUnixTime(item.CreateTime),
			formatUnixTime(item.PaymentTime),
			oneLine(item.ClientIP),
			oneLine(item.Device),
			oneLine(item.OrderRemark),
		)
	}
}

func formatTime(t time.Time) string {
	if t.IsZero() {
		return "-"
	}
	return t.Format("2006-01-02 15:04:05")
}

func formatUnixTime(v int64) string {
	if v <= 0 {
		return "-"
	}
	if v > 1_000_000_000_000 {
		return time.UnixMilli(v).Format("2006-01-02 15:04:05")
	}
	return time.Unix(v, 0).Format("2006-01-02 15:04:05")
}

func oneLine(s string) string {
	s = strings.ReplaceAll(s, "\t", " ")
	s = strings.ReplaceAll(s, "\r", " ")
	s = strings.ReplaceAll(s, "\n", " ")
	return strings.TrimSpace(s)
}

func abs64(v int64) int64 {
	if v < 0 {
		return -v
	}
	return v
}

func must(err error, step string) {
	if err == nil {
		return
	}
	fmt.Fprintf(os.Stderr, "%s: %v\n", step, err)
	os.Exit(1)
}
