package mysql

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"fmt"
	"hash"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
)

const (
	walletOutboxPurgeAuthorizationState = "RESTORE_VERIFIED"
	walletOutboxPurgeReceiptState       = "PURGED"
	walletOutboxPurgePolicyFinancialV1  = "financial-v1"
)

// financial-v1 是显式正向安全集合：只覆盖已经确认不被正常在线查询回读的账务事实。
// 未知新事件、礼物/资源目录事件和低频资源/VIP 控制事件默认永久留热，新增清理类型必须发布新 policy version。
var walletOutboxPurgeFinancialV1EventTypes = []string{
	"AgencyPointCredited",
	"HostPeriodDiamondCredited",
	"HostPointCredited",
	"WalletAgencyOpeningRewardCredited",
	"WalletBalanceChanged",
	"WalletCoinSellerCoinCompensated",
	"WalletCoinSellerStockPurchased",
	"WalletCoinSellerSubTransferred",
	"WalletCoinSellerTransferred",
	"WalletGiftDebited",
	"WalletGiftIncomeCredited",
	"WalletGooglePaymentCredited",
	"WalletInviteActivityRewardCredited",
	"WalletLuckyGiftRewardCredited",
	"WalletRechargeRecorded",
	"WalletRedPacketClaimed",
	"WalletRedPacketCreated",
	"WalletRedPacketRefunded",
	"WalletRoomTurnoverRewardCredited",
	"WalletTaskRewardCredited",
	"WalletWheelRewardCredited",
}

// WalletOutboxPurgeReceipt 是一个已经提交的精确 PK 清理批次。
// DeletedCount 为零时表示本轮没有成熟候选，此时 PurgeID 为空且数据库不会写空审计行。
type WalletOutboxPurgeReceipt struct {
	AppCode                 string
	PurgeID                 string
	State                   string
	PolicyVersion           string
	CutoffMS                int64
	ReceiptVerifiedBeforeMS int64
	CandidateSHA256         string
	CandidateCount          int
	DeletedCount            int
	FirstCursor             WalletOutboxArchiveCursor
	LastCursor              WalletOutboxArchiveCursor
	PurgedAtMS              int64
}

type walletOutboxPurgeCandidate struct {
	EventID     string
	UpdatedAtMS int64
}

// RequireWalletOutboxPurgeAuthorization 要求生产 wallet DB 已手工固化一条与隔离库
// RESTORE_VERIFIED 回执精确匹配的授权证据。迁移不会自动插入该表，因此仅部署人员
// 明确核验过的 (App, archive batch, manifest hash) 能打开自动清理总闸。
func (r *Repository) RequireWalletOutboxPurgeAuthorization(ctx context.Context, appCode, batchID, manifestSHA string) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx, normalizedAppCode, err := walletOutboxPurgeContext(ctx, appCode)
	if err != nil {
		return err
	}
	batchID, err = normalizeWalletOutboxPurgeSHA256("archive_batch_id", batchID)
	if err != nil {
		return err
	}
	manifestSHA, err = normalizeWalletOutboxPurgeSHA256("manifest_sha256", manifestSHA)
	if err != nil {
		return err
	}

	var marker int
	err = r.db.QueryRowContext(ctx, `
		SELECT 1
		FROM wallet_outbox_archive_purge_authorizations auth FORCE INDEX (PRIMARY)
		JOIN wallet_outbox_archive_receipts receipt FORCE INDEX (PRIMARY)
		  ON receipt.app_code = auth.app_code AND receipt.batch_id = auth.archive_batch_id
		 AND receipt.manifest_sha256 = auth.manifest_sha256
		 AND receipt.schema_version = auth.schema_version AND receipt.row_count = auth.row_count
		 AND receipt.data_crc64 = auth.data_crc64 AND receipt.manifest_crc64 = auth.manifest_crc64
		 AND receipt.success_crc64 = auth.success_crc64
		WHERE auth.app_code = ? AND auth.archive_batch_id = ? AND auth.manifest_sha256 = ?
		  AND auth.state = ? AND receipt.state = ?
		LIMIT 1`,
		normalizedAppCode, batchID, manifestSHA, walletOutboxPurgeAuthorizationState, walletOutboxArchiveStateVerified,
	).Scan(&marker)
	if errors.Is(err, sql.ErrNoRows) {
		return xerr.New(xerr.Conflict, "wallet outbox purge restore rehearsal is not authorized")
	}
	return err
}

// PurgeDeliveredWalletOutboxBatch 在单个 READ COMMITTED 事务内锁定、删除并审计至多 limit 条旧事实。
// 调用方必须已经持有 AcquireWalletOutboxArchiveAppLock 返回的 App 级 GET_LOCK。候选只能从
// exact member 表产生；每一行都重新连接当前 outbox 和 VERIFIED receipt，杜绝用批次首尾游标
// 推断中间行、迟到写入或旧 receipt 自动获得删除资格。
func (r *Repository) PurgeDeliveredWalletOutboxBatch(ctx context.Context, appCode, policyVersion string, cutoffMS, receiptVerifiedBeforeMS int64, limit int, nowMS int64) (WalletOutboxPurgeReceipt, error) {
	if r == nil || r.db == nil {
		return WalletOutboxPurgeReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx, normalizedAppCode, err := walletOutboxPurgeContext(ctx, appCode)
	if err != nil {
		return WalletOutboxPurgeReceipt{}, err
	}
	eventTypes, err := walletOutboxPurgeEventTypes(policyVersion)
	if err != nil {
		return WalletOutboxPurgeReceipt{}, err
	}
	if cutoffMS <= 0 || receiptVerifiedBeforeMS <= 0 || nowMS <= 0 || cutoffMS >= nowMS || receiptVerifiedBeforeMS >= nowMS {
		return WalletOutboxPurgeReceipt{}, xerr.New(xerr.InvalidArgument, "wallet outbox purge cutoff, receipt grace, and now are invalid")
	}
	if limit <= 0 || limit > 500 {
		return WalletOutboxPurgeReceipt{}, xerr.New(xerr.InvalidArgument, "wallet outbox purge limit must be within [1,500]")
	}

	tx, err := r.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
	if err != nil {
		return WalletOutboxPurgeReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()
	receipt := WalletOutboxPurgeReceipt{
		AppCode:                 normalizedAppCode,
		State:                   walletOutboxPurgeReceiptState,
		PolicyVersion:           policyVersion,
		CutoffMS:                cutoffMS,
		ReceiptVerifiedBeforeMS: receiptVerifiedBeforeMS,
		PurgedAtMS:              nowMS,
	}
	candidates, err := selectWalletOutboxPurgeCandidates(ctx, tx, normalizedAppCode, cutoffMS, receiptVerifiedBeforeMS, eventTypes, limit)
	if err != nil {
		return WalletOutboxPurgeReceipt{}, err
	}
	if len(candidates) == 0 {
		return receipt, nil
	}

	receipt.CandidateSHA256 = hashWalletOutboxPurgeCandidates(normalizedAppCode, candidates)
	receipt.CandidateCount = len(candidates)
	receipt.FirstCursor = WalletOutboxArchiveCursor{UpdatedAtMS: candidates[0].UpdatedAtMS, EventID: candidates[0].EventID}
	last := candidates[len(candidates)-1]
	receipt.LastCursor = WalletOutboxArchiveCursor{UpdatedAtMS: last.UpdatedAtMS, EventID: last.EventID}
	receipt.PurgeID = hashWalletOutboxPurgeReceipt(receipt)

	deleted, err := deleteWalletOutboxPurgeCandidates(ctx, tx, normalizedAppCode, cutoffMS, eventTypes, candidates)
	if err != nil {
		return WalletOutboxPurgeReceipt{}, err
	}
	if deleted != int64(len(candidates)) {
		return WalletOutboxPurgeReceipt{}, fmt.Errorf("wallet outbox purge candidate/delete mismatch: candidates=%d deleted=%d", len(candidates), deleted)
	}
	deletedMembers, err := deleteWalletOutboxArchiveMembers(ctx, tx, normalizedAppCode, candidates)
	if err != nil {
		return WalletOutboxPurgeReceipt{}, err
	}
	if deletedMembers != int64(len(candidates)) {
		// outbox 与 member 删除必须共同提交；member 数不一致时回滚 outbox 删除，避免留下无法审计的半完成批次。
		return WalletOutboxPurgeReceipt{}, fmt.Errorf("wallet outbox purge member/delete mismatch: candidates=%d deleted_members=%d", len(candidates), deletedMembers)
	}
	receipt.DeletedCount = int(deleted)
	if err := insertWalletOutboxPurgeReceipt(ctx, tx, receipt); err != nil {
		return WalletOutboxPurgeReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return WalletOutboxPurgeReceipt{}, err
	}
	return receipt, nil
}

func selectWalletOutboxPurgeCandidates(ctx context.Context, tx *sql.Tx, appCode string, cutoffMS, receiptVerifiedBeforeMS int64, eventTypes []string, limit int) ([]walletOutboxPurgeCandidate, error) {
	placeholders := walletOutboxPurgePlaceholders(len(eventTypes))
	args := make([]any, 0, len(eventTypes)+6)
	args = append(args, appCode, cutoffMS, walletOutboxArchiveStateVerified, receiptVerifiedBeforeMS, outboxStatusDelivered)
	for _, eventType := range eventTypes {
		args = append(args, eventType)
	}
	args = append(args, limit)
	rows, err := tx.QueryContext(ctx, `
		SELECT STRAIGHT_JOIN m.event_id, m.updated_at_ms
		FROM wallet_outbox_archive_members m FORCE INDEX (idx_wallet_outbox_archive_member_purge)
		JOIN wallet_outbox_archive_receipts ar FORCE INDEX (PRIMARY)
		  ON ar.app_code = m.app_code AND ar.batch_id = m.batch_id
		 AND ar.manifest_sha256 = m.manifest_sha256 AND ar.verified_at_ms = m.receipt_verified_at_ms
		JOIN wallet_outbox wo FORCE INDEX (PRIMARY)
		  ON wo.app_code = m.app_code AND wo.event_id = m.event_id AND wo.updated_at_ms = m.updated_at_ms
		WHERE m.app_code = ? AND m.updated_at_ms < ?
		  AND ar.state = ? AND ar.verified_at_ms < ?
		  AND wo.status = ? AND wo.event_type IN (`+placeholders+`)
		ORDER BY m.updated_at_ms ASC, m.event_id ASC
		LIMIT ?
		FOR UPDATE SKIP LOCKED`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	candidates := make([]walletOutboxPurgeCandidate, 0, limit)
	for rows.Next() {
		var candidate walletOutboxPurgeCandidate
		if err := rows.Scan(&candidate.EventID, &candidate.UpdatedAtMS); err != nil {
			return nil, err
		}
		candidates = append(candidates, candidate)
	}
	return candidates, rows.Err()
}

func deleteWalletOutboxPurgeCandidates(ctx context.Context, tx *sql.Tx, appCode string, cutoffMS int64, eventTypes []string, candidates []walletOutboxPurgeCandidate) (int64, error) {
	primaryKeys, args, err := walletOutboxPurgeExactKeys(appCode, candidates)
	if err != nil {
		return 0, err
	}
	args = append(args, appCode, outboxStatusDelivered, cutoffMS)
	for _, eventType := range eventTypes {
		args = append(args, eventType)
	}
	// 候选 SELECT 已锁住这些行；删除仍逐键复核 app/event/updated、delivered、cutoff 和正向 policy，
	// 即使未来有人修改候选 JOIN，也不能把未授权类型或时间发生变化的行带入删除。
	result, err := tx.ExecContext(ctx, `
		DELETE wo
		FROM wallet_outbox wo FORCE INDEX (PRIMARY)
		WHERE (wo.app_code, wo.event_id, wo.updated_at_ms) IN (`+primaryKeys+`)
		  AND wo.app_code = ? AND wo.status = ? AND wo.updated_at_ms < ?
		  AND wo.event_type IN (`+walletOutboxPurgePlaceholders(len(eventTypes))+`)`, args...)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

func deleteWalletOutboxArchiveMembers(ctx context.Context, tx *sql.Tx, appCode string, candidates []walletOutboxPurgeCandidate) (int64, error) {
	primaryKeys, args, err := walletOutboxPurgeExactKeys(appCode, candidates)
	if err != nil {
		return 0, err
	}
	result, err := tx.ExecContext(ctx, `
		DELETE m
		FROM wallet_outbox_archive_members m FORCE INDEX (PRIMARY)
		WHERE (m.app_code, m.event_id, m.updated_at_ms) IN (`+primaryKeys+`)`, args...)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

func walletOutboxPurgeExactKeys(appCode string, candidates []walletOutboxPurgeCandidate) (string, []any, error) {
	if len(candidates) == 0 || len(candidates) > 500 {
		return "", nil, xerr.New(xerr.InvalidArgument, "wallet outbox purge candidate count must be within [1,500]")
	}
	keys := make([]string, 0, len(candidates))
	args := make([]any, 0, len(candidates)*3)
	for _, candidate := range candidates {
		keys = append(keys, "(?, ?, ?)")
		args = append(args, appCode, candidate.EventID, candidate.UpdatedAtMS)
	}
	return strings.Join(keys, ","), args, nil
}

func insertWalletOutboxPurgeReceipt(ctx context.Context, tx *sql.Tx, receipt WalletOutboxPurgeReceipt) error {
	result, err := tx.ExecContext(ctx, `
		INSERT INTO wallet_outbox_archive_purge_receipts (
		  app_code, purge_id, state, policy_version, cutoff_ms, receipt_verified_before_ms,
		  candidate_sha256, candidate_count, deleted_count,
		  first_updated_at_ms, first_event_id, last_updated_at_ms, last_event_id,
		  purged_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		receipt.AppCode, receipt.PurgeID, receipt.State, receipt.PolicyVersion, receipt.CutoffMS, receipt.ReceiptVerifiedBeforeMS,
		receipt.CandidateSHA256, receipt.CandidateCount, receipt.DeletedCount,
		receipt.FirstCursor.UpdatedAtMS, receipt.FirstCursor.EventID, receipt.LastCursor.UpdatedAtMS, receipt.LastCursor.EventID,
		receipt.PurgedAtMS, receipt.PurgedAtMS, receipt.PurgedAtMS,
	)
	if err != nil {
		return err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return err
	}
	if affected != 1 {
		return fmt.Errorf("wallet outbox purge receipt insert affected %d rows", affected)
	}
	return nil
}

func walletOutboxPurgeContext(ctx context.Context, appCode string) (context.Context, string, error) {
	if strings.TrimSpace(appCode) == "" {
		return ctx, "", xerr.New(xerr.InvalidArgument, "wallet outbox purge app_code is required")
	}
	ctx = contextWithCommandApp(ctx, appCode)
	normalized := appcode.FromContext(ctx)
	// 可清理租户由启动配置的显式 allowlist、恢复证明以及数据库授权三重约束；
	// 仓储层不再把产品名称写进代码，否则新增租户会形成隐蔽的行为分支。
	return ctx, normalized, nil
}

func walletOutboxPurgeEventTypes(policyVersion string) ([]string, error) {
	policyVersion = strings.TrimSpace(policyVersion)
	if policyVersion != walletOutboxPurgePolicyFinancialV1 {
		return nil, xerr.New(xerr.InvalidArgument, "wallet outbox purge policy_version is unsupported")
	}
	return walletOutboxPurgeFinancialV1EventTypes, nil
}

func normalizeWalletOutboxPurgeSHA256(field string, value string) (string, error) {
	value = strings.ToLower(strings.TrimSpace(value))
	if len(value) != sha256.Size*2 {
		return "", xerr.New(xerr.InvalidArgument, field+" must be a SHA-256 hex digest")
	}
	if _, err := hex.DecodeString(value); err != nil {
		return "", xerr.New(xerr.InvalidArgument, field+" must be a SHA-256 hex digest")
	}
	return value, nil
}

func walletOutboxPurgePlaceholders(count int) string {
	values := make([]string, count)
	for index := range values {
		values[index] = "?"
	}
	return strings.Join(values, ",")
}

func hashWalletOutboxPurgeCandidates(appCode string, candidates []walletOutboxPurgeCandidate) string {
	digest := sha256.New()
	writeWalletOutboxPurgeHashString(digest, appCode)
	writeWalletOutboxPurgeHashUint64(digest, uint64(len(candidates)))
	for _, candidate := range candidates {
		// 候选已按 (updated_at_ms,event_id) 排序；哈希内容采用精确 PK 和归档时 updated_at 快照。
		writeWalletOutboxPurgeHashString(digest, candidate.EventID)
		writeWalletOutboxPurgeHashUint64(digest, uint64(candidate.UpdatedAtMS))
	}
	return hex.EncodeToString(digest.Sum(nil))
}

func hashWalletOutboxPurgeReceipt(receipt WalletOutboxPurgeReceipt) string {
	digest := sha256.New()
	writeWalletOutboxPurgeHashString(digest, receipt.AppCode)
	writeWalletOutboxPurgeHashString(digest, receipt.PolicyVersion)
	writeWalletOutboxPurgeHashString(digest, receipt.CandidateSHA256)
	writeWalletOutboxPurgeHashUint64(digest, uint64(receipt.CutoffMS))
	writeWalletOutboxPurgeHashUint64(digest, uint64(receipt.ReceiptVerifiedBeforeMS))
	writeWalletOutboxPurgeHashUint64(digest, uint64(receipt.PurgedAtMS))
	return hex.EncodeToString(digest.Sum(nil))
}

func writeWalletOutboxPurgeHashString(digest hash.Hash, value string) {
	// 长度前缀消除字段拼接歧义，使相同候选在不同进程上得到完全一致的审计指纹。
	var size [4]byte
	binary.BigEndian.PutUint32(size[:], uint32(len(value)))
	_, _ = digest.Write(size[:])
	_, _ = digest.Write([]byte(value))
}

func writeWalletOutboxPurgeHashUint64(digest hash.Hash, value uint64) {
	var encoded [8]byte
	binary.BigEndian.PutUint64(encoded[:], value)
	_, _ = digest.Write(encoded[:])
}
