package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
)

func ensureResourceGroupSnapshotSchema(ctx context.Context, db *sql.DB) error {
	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS resource_group_snapshots (
		  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
		  snapshot_id VARCHAR(96) NOT NULL COMMENT '不可变资源组快照 ID',
		  pin_key VARCHAR(192) NOT NULL COMMENT '发布方业务版本幂等键',
		  source_group_id BIGINT NOT NULL COMMENT '来源资源组 ID',
		  version_no BIGINT NOT NULL COMMENT '来源资源组递增快照版本',
		  snapshot_hash CHAR(64) NOT NULL COMMENT '完整快照 SHA-256',
		  snapshot_json JSON NOT NULL COMMENT '含资源详情和钱包资产的完整不可变快照',
		  source_group_updated_at_ms BIGINT NOT NULL COMMENT '锁定时来源资源组版本',
		  source_content_hash CHAR(64) NOT NULL COMMENT '资源组、组项和嵌套资源的规范化内容哈希',
		  created_by_user_id BIGINT NOT NULL,
		  created_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, snapshot_id),
		  UNIQUE KEY uk_resource_group_snapshot_pin (app_code, pin_key),
		  UNIQUE KEY uk_resource_group_snapshot_version (app_code, source_group_id, version_no),
		  KEY idx_resource_group_snapshot_source (app_code, source_group_id, created_at_ms)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源组发布态不可变奖励快照'`); err != nil {
		return err
	}
	var database string
	if err := db.QueryRowContext(ctx, `SELECT DATABASE()`).Scan(&database); err != nil {
		return err
	}
	var sourceVersionColumn int
	if err := db.QueryRowContext(ctx, `
		SELECT COUNT(*) FROM information_schema.COLUMNS
		WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'resource_group_snapshots' AND COLUMN_NAME = 'source_group_updated_at_ms'`, database).
		Scan(&sourceVersionColumn); err != nil {
		return err
	}
	if sourceVersionColumn == 0 {
		if _, err := db.ExecContext(ctx, `
			ALTER TABLE resource_group_snapshots
			ADD COLUMN source_group_updated_at_ms BIGINT NOT NULL DEFAULT 0 AFTER snapshot_json`); err != nil {
			return err
		}
		if _, err := db.ExecContext(ctx, `
			UPDATE resource_group_snapshots
			SET source_group_updated_at_ms = CAST(JSON_UNQUOTE(JSON_EXTRACT(snapshot_json, '$.UpdatedAtMS')) AS SIGNED)
			WHERE source_group_updated_at_ms = 0
			  AND JSON_EXTRACT(snapshot_json, '$.UpdatedAtMS') IS NOT NULL`); err != nil {
			return err
		}
	}
	var invalidSourceVersions int64
	if err := db.QueryRowContext(ctx, `
		SELECT COUNT(*) FROM resource_group_snapshots WHERE source_group_updated_at_ms <= 0`).Scan(&invalidSourceVersions); err != nil {
		return err
	}
	if invalidSourceVersions > 0 {
		return xerr.New(xerr.Internal, "resource group snapshots contain invalid source versions")
	}
	var sourceHashColumn int
	if err := db.QueryRowContext(ctx, `
		SELECT COUNT(*) FROM information_schema.COLUMNS
		WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'resource_group_snapshots' AND COLUMN_NAME = 'source_content_hash'`, database).
		Scan(&sourceHashColumn); err != nil {
		return err
	}
	if sourceHashColumn == 0 {
		if _, err := db.ExecContext(ctx, `
			ALTER TABLE resource_group_snapshots
			ADD COLUMN source_content_hash CHAR(64) NOT NULL DEFAULT '' AFTER source_group_updated_at_ms`); err != nil {
			return err
		}
		// Existing snapshots were written from the same canonical group JSON. Backfill the integrity hash so old
		// owner snapshots remain grantable after the stronger source-version contract is deployed.
		if _, err := db.ExecContext(ctx, `
			UPDATE resource_group_snapshots SET source_content_hash = snapshot_hash
			WHERE source_content_hash = ''`); err != nil {
			return err
		}
	}
	return nil
}

// PinResourceGroupSnapshot locks and validates the mutable owner aggregate exactly once for a caller's
// business-version pin key. A retry returns the original bytes even when the source group was edited later.
func (r *Repository) PinResourceGroupSnapshot(ctx context.Context, command resourcedomain.PinResourceGroupSnapshotCommand) (resourcedomain.ResourceGroupSnapshot, error) {
	if r == nil || r.db == nil {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	command.PinKey = strings.TrimSpace(command.PinKey)
	command.RequiredRegionIDs = normalizeRequiredRegionIDs(command.RequiredRegionIDs)
	if command.AppCode == "" || command.GroupID <= 0 || command.OperatorUserID <= 0 || command.ExpectedGroupUpdatedAtMS <= 0 ||
		!validSHA256(command.ExpectedSourceContentHash) || command.PinKey == "" || len(command.PinKey) > 192 {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.InvalidArgument, "resource group snapshot command is incomplete")
	}
	for _, regionID := range command.RequiredRegionIDs {
		if regionID <= 0 {
			return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.InvalidArgument, "required_region_ids must contain positive active region IDs")
		}
	}
	ctx = appcode.WithContext(ctx, command.AppCode)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	defer func() { _ = tx.Rollback() }()

	if existing, found, err := getResourceGroupSnapshotByPinKey(ctx, tx, command.AppCode, command.PinKey, true); err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	} else if found {
		if existing.SourceGroupID != command.GroupID || existing.SourceContentHash != command.ExpectedSourceContentHash {
			return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Conflict, "resource group snapshot pin key belongs to another group")
		}
		if err := validatePinnedGiftRegionCoverage(existing.Group.Items, command.RequiredRegionIDs, command.RequiredAllRegions); err != nil {
			return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Conflict, err.Error())
		}
		return existing, nil
	}
	group, err := r.getResourceGroup(ctx, tx, command.GroupID, true)
	if err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	if group.Status != resourcedomain.StatusActive {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Conflict, "resource group is disabled")
	}
	if group.UpdatedAtMS != command.ExpectedGroupUpdatedAtMS {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Conflict, "resource group changed before snapshot pin")
	}
	group.Items, err = r.listResourceGroupItemsTx(ctx, tx, command.GroupID, true)
	if err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	if len(group.Items) == 0 {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.InvalidArgument, "resource group has no items")
	}
	pinNowMS := time.Now().UTC().UnixMilli()
	if err := r.enrichResourceGroupGiftSnapshots(ctx, tx, group.Items, pinNowMS, true); err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Conflict, "gift resource cannot be pinned: "+err.Error())
	}
	if err := validateActiveResourceGroupItems(group.Items); err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	for _, item := range group.Items {
		if resourcedomain.NormalizeResourceType(item.Resource.ResourceType) != resourcedomain.TypeGift {
			continue
		}
		if err := validatePinnedGiftConfigs(command.AppCode, item.Resource, item.GiftConfigs); err != nil {
			return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Conflict, "gift resource cannot be pinned: "+err.Error())
		}
	}
	if err := validatePinnedGiftRegionCoverage(group.Items, command.RequiredRegionIDs, command.RequiredAllRegions); err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Conflict, err.Error())
	}
	snapshotJSON, err := resourcedomain.CanonicalResourceGroupJSON(group)
	if err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	sourceContentHash, err := resourcedomain.ResourceGroupContentHash(group)
	if err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	if sourceContentHash != command.ExpectedSourceContentHash {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Conflict, "resource group content changed before snapshot pin")
	}
	var versionNo int64
	if err := tx.QueryRowContext(ctx, `
		SELECT COALESCE(MAX(version_no), 0) + 1 FROM resource_group_snapshots
		WHERE app_code = ? AND source_group_id = ?`, command.AppCode, command.GroupID).Scan(&versionNo); err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	nowMS := pinNowMS
	snapshot := resourcedomain.ResourceGroupSnapshot{
		AppCode: command.AppCode, SnapshotID: "rgs_" + stableHash(command.AppCode+"|"+command.PinKey),
		PinKey: command.PinKey, SourceGroupID: command.GroupID, VersionNo: versionNo,
		SnapshotHash: sourceContentHash, Group: group,
		CreatedByUserID: command.OperatorUserID, CreatedAtMS: nowMS, SourceGroupUpdatedAtMS: group.UpdatedAtMS,
		SourceContentHash: sourceContentHash,
	}
	result, err := tx.ExecContext(ctx, `
		INSERT IGNORE INTO resource_group_snapshots (
		  app_code, snapshot_id, pin_key, source_group_id, version_no, snapshot_hash, snapshot_json,
		  source_group_updated_at_ms, source_content_hash, created_by_user_id, created_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?)`,
		snapshot.AppCode, snapshot.SnapshotID, snapshot.PinKey, snapshot.SourceGroupID, snapshot.VersionNo,
		snapshot.SnapshotHash, string(snapshotJSON), snapshot.SourceGroupUpdatedAtMS, snapshot.SourceContentHash,
		snapshot.CreatedByUserID, snapshot.CreatedAtMS)
	if err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	if affected == 0 {
		existing, found, err := getResourceGroupSnapshotByPinKey(ctx, tx, command.AppCode, command.PinKey, true)
		if err != nil {
			return resourcedomain.ResourceGroupSnapshot{}, err
		}
		if !found || existing.SourceGroupID != command.GroupID || existing.SourceContentHash != command.ExpectedSourceContentHash {
			return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Conflict, "resource group snapshot pin conflict")
		}
		if err := validatePinnedGiftRegionCoverage(existing.Group.Items, command.RequiredRegionIDs, command.RequiredAllRegions); err != nil {
			return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Conflict, err.Error())
		}
		return existing, nil
	}
	if err := tx.Commit(); err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	return snapshot, nil
}

func normalizeRequiredRegionIDs(values []int64) []int64 {
	seen := make(map[int64]struct{}, len(values))
	out := make([]int64, 0, len(values))
	for _, value := range values {
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	sort.Slice(out, func(left, right int) bool { return out[left] < out[right] })
	return out
}

func (r *Repository) GetResourceGroupSnapshot(ctx context.Context, snapshotID string) (resourcedomain.ResourceGroupSnapshot, error) {
	if r == nil || r.db == nil {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	item, found, err := getResourceGroupSnapshot(ctx, r.db, appcode.FromContext(ctx), strings.TrimSpace(snapshotID), false)
	if err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, err
	}
	if !found {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.NotFound, "resource group snapshot not found")
	}
	return item, nil
}

func getResourceGroupSnapshotByPinKey(ctx context.Context, q resourceSnapshotQuerier, appCode, pinKey string, forUpdate bool) (resourcedomain.ResourceGroupSnapshot, bool, error) {
	query := `SELECT app_code, snapshot_id, pin_key, source_group_id, version_no, snapshot_hash,
	                 CAST(snapshot_json AS CHAR), source_group_updated_at_ms, source_content_hash, created_by_user_id, created_at_ms
	          FROM resource_group_snapshots WHERE app_code = ? AND pin_key = ?`
	if forUpdate {
		query += " FOR UPDATE"
	}
	return scanResourceGroupSnapshot(q.QueryRowContext(ctx, query, appCode, pinKey))
}

func getResourceGroupSnapshot(ctx context.Context, q resourceSnapshotQuerier, appCode, snapshotID string, forUpdate bool) (resourcedomain.ResourceGroupSnapshot, bool, error) {
	query := `SELECT app_code, snapshot_id, pin_key, source_group_id, version_no, snapshot_hash,
	                 CAST(snapshot_json AS CHAR), source_group_updated_at_ms, source_content_hash, created_by_user_id, created_at_ms
	          FROM resource_group_snapshots WHERE app_code = ? AND snapshot_id = ?`
	if forUpdate {
		query += " FOR UPDATE"
	}
	return scanResourceGroupSnapshot(q.QueryRowContext(ctx, query, appCode, snapshotID))
}

func scanResourceGroupSnapshot(row resourceSnapshotScanner) (resourcedomain.ResourceGroupSnapshot, bool, error) {
	var item resourcedomain.ResourceGroupSnapshot
	var snapshotJSON string
	err := row.Scan(&item.AppCode, &item.SnapshotID, &item.PinKey, &item.SourceGroupID, &item.VersionNo,
		&item.SnapshotHash, &snapshotJSON, &item.SourceGroupUpdatedAtMS, &item.SourceContentHash, &item.CreatedByUserID, &item.CreatedAtMS)
	if errors.Is(err, sql.ErrNoRows) {
		return resourcedomain.ResourceGroupSnapshot{}, false, nil
	}
	if err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, false, err
	}
	if err := json.Unmarshal([]byte(snapshotJSON), &item.Group); err != nil {
		return resourcedomain.ResourceGroupSnapshot{}, false, fmt.Errorf("decode resource group snapshot: %w", err)
	}
	canonicalJSON, marshalErr := resourcedomain.CanonicalResourceGroupJSON(item.Group)
	if marshalErr != nil {
		return resourcedomain.ResourceGroupSnapshot{}, false, marshalErr
	}
	if stableHash(string(canonicalJSON)) != item.SnapshotHash || item.SourceContentHash != item.SnapshotHash ||
		item.Group.GroupID != item.SourceGroupID || item.Group.UpdatedAtMS != item.SourceGroupUpdatedAtMS {
		return resourcedomain.ResourceGroupSnapshot{}, false, xerr.New(xerr.Internal, "resource group snapshot integrity check failed")
	}
	return item, true, nil
}

func validSHA256(value string) bool {
	value = strings.TrimSpace(value)
	if len(value) != 64 {
		return false
	}
	for _, character := range value {
		if (character < '0' || character > '9') && (character < 'a' || character > 'f') {
			return false
		}
	}
	return true
}

type resourceSnapshotQuerier interface {
	QueryRowContext(context.Context, string, ...any) *sql.Row
}

type resourceSnapshotScanner interface {
	Scan(...any) error
}
