package appuser

import (
	"context"
	"encoding/csv"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/model"
	"hyapp-admin-server/internal/modules/shared"
	"hyapp-admin-server/internal/response"

	"github.com/gin-gonic/gin"
)

const AppUserExportJobType = "app-user-export"

type appUserExportJobStore interface {
	CreateJob(job *model.AdminJob) error
}

type appUserExportPayload struct {
	AppCode string    `json:"appCode"`
	Query   listQuery `json:"query"`
}

type appUserExportJobResponse struct {
	JobID        uint   `json:"jobId"`
	Status       string `json:"status"`
	SnapshotAtMs int64  `json:"snapshotAtMs"`
}

type appUserExportResult struct {
	JobID        uint   `json:"jobId"`
	Rows         int64  `json:"rows"`
	Artifact     string `json:"artifact"`
	DownloadPath string `json:"downloadPath"`
	SnapshotAtMs int64  `json:"snapshotAtMs"`
}

func (s *Service) CreateExportJob(ctx context.Context, actor shared.Actor, query listQuery) (*appUserExportJobResponse, error) {
	if s.jobStore == nil || !s.jobsConfig.Enabled {
		return nil, errors.New("admin jobs are not enabled")
	}
	query = normalizeListQuery(query)
	query = stableAppUserExportQuery(query)
	query.Page = 1
	query.PageSize = 100
	query.SnapshotAtMs = nowMillis()
	payload := appUserExportPayload{
		AppCode: appctx.FromContext(ctx),
		Query:   query,
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}
	job := model.AdminJob{
		Type:          AppUserExportJobType,
		Status:        model.JobStatusPending,
		PayloadJSON:   string(body),
		MaxAttempts:   s.jobsConfig.MaxAttempts,
		CreatedBy:     actor.UserID,
		CreatedByName: strings.TrimSpace(actor.Username),
	}
	if job.MaxAttempts <= 0 {
		job.MaxAttempts = 3
	}
	if err := s.jobStore.CreateJob(&job); err != nil {
		return nil, err
	}
	return &appUserExportJobResponse{JobID: job.ID, Status: job.Status, SnapshotAtMs: query.SnapshotAtMs}, nil
}

// HandleExportJob 使用创建任务时固化的 App 与筛选条件逐页读取；任务执行期间不会重新解释前端 query。
func (s *Service) HandleExportJob(ctx context.Context, job *model.AdminJob) (string, string, error) {
	if job == nil || job.ID == 0 {
		return "", "", errors.New("job is required")
	}
	var payload appUserExportPayload
	if err := json.Unmarshal([]byte(job.PayloadJSON), &payload); err != nil {
		return "", "", err
	}
	payload.AppCode = strings.TrimSpace(payload.AppCode)
	payload.Query = normalizeListQuery(payload.Query)
	payload.Query = stableAppUserExportQuery(payload.Query)
	if payload.AppCode == "" || payload.Query.SnapshotAtMs <= 0 {
		return "", "", ErrInvalidArgument
	}

	artifactDir := strings.TrimSpace(s.jobsConfig.ArtifactDir)
	if artifactDir == "" {
		artifactDir = "storage/exports"
	}
	if err := os.MkdirAll(artifactDir, 0o750); err != nil {
		return "", "", err
	}
	file, err := os.CreateTemp(artifactDir, fmt.Sprintf("app-user-export-%d-*.csv", job.ID))
	if err != nil {
		return "", "", err
	}
	artifactName := filepath.Base(file.Name())
	succeeded := false
	defer func() {
		_ = file.Close()
		if !succeeded {
			_ = os.Remove(file.Name())
		}
	}()

	// UTF-8 BOM 让运营直接使用 Excel 打开中文昵称、国家和身份时不出现乱码。
	if _, err := file.WriteString("\xEF\xBB\xBF"); err != nil {
		return "", "", err
	}
	writer := csv.NewWriter(file)
	if err := writer.Write(appUserExportHeaders()); err != nil {
		return "", "", err
	}

	exportCtx := appctx.WithContext(ctx, payload.AppCode)
	query := payload.Query
	query.Page = 1
	query.PageSize = 100
	var exported int64
	for {
		items, _, err := s.ListUsers(exportCtx, query)
		if err != nil {
			return "", "", err
		}
		for i := range items {
			if err := writer.Write(appUserExportRow(items[i])); err != nil {
				return "", "", err
			}
		}
		exported += int64(len(items))
		if len(items) < query.PageSize {
			break
		}
		last := items[len(items)-1]
		cursorUserID, err := strconv.ParseInt(last.UserID, 10, 64)
		if err != nil || cursorUserID <= 0 || last.CreatedAtMs <= 0 {
			return "", "", errors.New("app user export cursor is invalid")
		}
		// 使用 (created_at_ms,user_id) keyset 而不是 OFFSET；任务运行期间新增用户或已读用户变更筛选字段时，
		// 后续批次仍不会位移、重复或跳过尚未读取的稳定主键区间。
		query.ExportCursorCreatedAtMs = last.CreatedAtMs
		query.ExportCursorUserID = cursorUserID
	}
	writer.Flush()
	if err := writer.Error(); err != nil {
		return "", "", err
	}
	if err := file.Sync(); err != nil {
		return "", "", err
	}
	if err := file.Close(); err != nil {
		return "", "", err
	}
	succeeded = true

	result := appUserExportResult{
		JobID:        job.ID,
		Rows:         exported,
		Artifact:     artifactName,
		DownloadPath: fmt.Sprintf("/api/v1/jobs/%d/artifact", job.ID),
		SnapshotAtMs: payload.Query.SnapshotAtMs,
	}
	resultJSON, err := json.Marshal(result)
	if err != nil {
		return "", "", err
	}
	return string(resultJSON), artifactName, nil
}

func stableAppUserExportQuery(query listQuery) listQuery {
	// 导出顺序不属于筛选语义，统一 created_at/user_id 倒序才能和固定 keyset 游标一致。
	// 这也绕开 coin 投影的全量内存排序，避免每一批重复读取形成 O(批次数×用户数)。
	query.SortBy = "created_at"
	query.SortDirection = "desc"
	return query
}

func (h *Handler) CreateExport(c *gin.Context) {
	job, err := h.service.CreateExportJob(c.Request.Context(), shared.ActorFromContext(c), parseListQuery(c))
	if err != nil {
		response.ServerError(c, "创建 App 用户导出任务失败")
		return
	}
	writeAppUserAuditLog(c, h.audit, "export-app-users", "app_user_exports", int64(job.JobID), "success", fmt.Sprintf("snapshot_at_ms=%d", job.SnapshotAtMs))
	response.OK(c, job)
}

func (h *Handler) HandleExportJob(ctx context.Context, job *model.AdminJob) (string, string, error) {
	return h.service.HandleExportJob(ctx, job)
}

func appUserExportHeaders() []string {
	return []string{
		"用户ID", "短ID", "靓号", "昵称", "头像", "性别", "生日", "年龄", "国家", "区域",
		"VIP等级", "VIP到期时间(ms)", "富豪真实等级", "富豪展示等级", "富豪临时目标", "富豪到期时间(ms)",
		"魅力真实等级", "魅力展示等级", "魅力临时目标", "魅力到期时间(ms)", "游戏等级", "身份",
		"注册设备", "金币", "钻石", "累计充值(USD分)", "状态", "封禁类型", "封禁到期时间(ms)", "封禁原因",
		"注册时间(ms)", "最近活跃时间(ms)", "最近成功登录时间(ms)", "最新操作人", "最新操作", "最新操作时间(ms)",
	}
}

func appUserExportRow(item AppUser) []string {
	age := ""
	if item.Age != nil {
		age = strconv.FormatInt(int64(*item.Age), 10)
	}
	banType := ""
	if item.Ban.Active {
		if item.Ban.Permanent {
			banType = "永久"
		} else {
			banType = "限时"
		}
	}
	operatorName := ""
	operatorAction := ""
	if item.LastOperator != nil {
		operatorName = item.LastOperator.Name
		operatorAction = item.LastOperator.Action
	}
	row := []string{
		item.UserID, item.DefaultDisplayUserID, firstNonEmptyString(item.PrettyDisplayUserID, item.PrettyID), item.Username, item.Avatar,
		item.Gender, item.Birth, age, firstNonEmptyString(item.CountryDisplayName, item.CountryName, item.Country), item.RegionName,
		strconv.FormatInt(int64(item.VIP.Level), 10), strconv.FormatInt(item.VIP.ExpiresAtMs, 10),
		strconv.FormatInt(int64(item.Levels.Wealth.RealLevel), 10), strconv.FormatInt(int64(item.Levels.Wealth.DisplayLevel), 10), strconv.FormatInt(int64(item.Levels.Wealth.TemporaryTargetLevel), 10), strconv.FormatInt(item.Levels.Wealth.ExpiresAtMs, 10),
		strconv.FormatInt(int64(item.Levels.Charm.RealLevel), 10), strconv.FormatInt(int64(item.Levels.Charm.DisplayLevel), 10), strconv.FormatInt(int64(item.Levels.Charm.TemporaryTargetLevel), 10), strconv.FormatInt(item.Levels.Charm.ExpiresAtMs, 10),
		strconv.FormatInt(int64(item.Levels.Game.DisplayLevel), 10), strings.Join(item.Roles, "|"), item.RegisterDevice,
		strconv.FormatInt(item.Coin, 10), strconv.FormatInt(item.Diamond, 10), strconv.FormatInt(item.CumulativeRechargeUSDMinor, 10),
		item.Status, banType, strconv.FormatInt(item.Ban.ExpiresAtMs, 10), item.Ban.Reason,
		strconv.FormatInt(item.CreatedAtMs, 10), strconv.FormatInt(item.LastActiveAtMs, 10), strconv.FormatInt(item.LastLoginAtMs, 10),
		operatorName, operatorAction, strconv.FormatInt(item.LastOperatedAtMs, 10),
	}
	for index := range row {
		row[index] = spreadsheetSafeCSVCell(row[index])
	}
	return row
}

func spreadsheetSafeCSVCell(value string) string {
	trimmed := strings.TrimLeft(value, " \t\r\n")
	if trimmed == "" {
		return value
	}
	switch trimmed[0] {
	case '=', '+', '-', '@':
		// Excel/Sheets 会把这些前缀解释为公式；前置单引号只影响表格解释，不丢失原始文本内容。
		return "'" + value
	default:
		return value
	}
}

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