package file

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/perfect-panel/server/internal/model/user"
	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/pkg/constant"
	"github.com/perfect-panel/server/pkg/xerr"
	"github.com/pkg/errors"
)

const (
	fileUploadInitStatus     = "init"
	fileUploadCompleteStatus = "completed"
	fileUploadRedisPrefix    = "file:upload:"
)

var safeFileNameRegexp = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)

type uploadMeta struct {
	FileID      string `json:"file_id"`
	UserID      int64  `json:"user_id"`
	BizType     string `json:"biz_type"`
	FileName    string `json:"file_name"`
	ContentType string `json:"content_type"`
	ObjectKey   string `json:"object_key"`
	Size        int64  `json:"size"`
	Sha256      string `json:"sha256"`
	Status      string `json:"status"`
	Etag        string `json:"etag,omitempty"`
	CreatedAt   int64  `json:"created_at"`
	CompletedAt int64  `json:"completed_at,omitempty"`
}

func currentUserFromContext(ctx context.Context) (*user.User, error) {
	u, ok := ctx.Value(constant.CtxKeyUser).(*user.User)
	if !ok || u == nil {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
	}
	return u, nil
}

func allowedContentTypeSet(csv string) map[string]struct{} {
	set := make(map[string]struct{})
	for _, item := range strings.Split(csv, ",") {
		item = strings.TrimSpace(strings.ToLower(item))
		if item == "" {
			continue
		}
		set[item] = struct{}{}
	}
	return set
}

func validateInitRequest(svcCtx *svc.ServiceContext, bizType, fileName, contentType string, size int64) error {
	if svcCtx.S3Store == nil || !svcCtx.Config.S3.Enable {
		return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "S3 upload is not enabled")
	}
	if strings.TrimSpace(bizType) == "" {
		return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "biz_type is required")
	}
	if strings.TrimSpace(fileName) == "" {
		return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "file_name is required")
	}
	if size <= 0 {
		return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "size must be greater than 0")
	}
	if svcCtx.Config.S3.MaxUploadSize > 0 && size > svcCtx.Config.S3.MaxUploadSize {
		return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "file size exceeds max limit")
	}
	allowed := allowedContentTypeSet(svcCtx.Config.S3.AllowedContentTypes)
	if len(allowed) > 0 {
		if _, ok := allowed[strings.ToLower(strings.TrimSpace(contentType))]; !ok {
			return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "content_type is not allowed")
		}
	}
	return nil
}

func buildFileID(userID int64, bizType, fileName string) string {
	h := sha1.New()
	_, _ = h.Write([]byte(fmt.Sprintf("%d:%s:%s:%d", userID, bizType, fileName, time.Now().UnixNano())))
	return hex.EncodeToString(h.Sum(nil))[:24]
}

func safeFileName(name string) string {
	base := filepath.Base(strings.TrimSpace(name))
	if base == "." || base == "/" || base == "" {
		return "file.bin"
	}
	safe := safeFileNameRegexp.ReplaceAllString(base, "_")
	if safe == "" {
		return "file.bin"
	}
	return safe
}

func buildObjectKey(prefix string, userID int64, bizType, fileID, fileName string, now time.Time) string {
	prefix = strings.Trim(prefix, "/")
	if prefix == "" {
		prefix = "app-upload"
	}
	return fmt.Sprintf("%s/%04d/%02d/%02d/%d/%s__%s",
		prefix,
		now.Year(),
		int(now.Month()),
		now.Day(),
		userID,
		safeFileName(fileName),
		fileID,
	)
}

func uploadRedisKey(fileID string) string {
	return fileUploadRedisPrefix + fileID
}

func saveUploadMeta(ctx context.Context, svcCtx *svc.ServiceContext, meta *uploadMeta, ttl time.Duration) error {
	data, err := json.Marshal(meta)
	if err != nil {
		return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal upload meta failed")
	}
	if err := svcCtx.Redis.Set(ctx, uploadRedisKey(meta.FileID), data, ttl).Err(); err != nil {
		return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "save upload meta failed")
	}
	return nil
}

func loadUploadMeta(ctx context.Context, svcCtx *svc.ServiceContext, fileID string) (*uploadMeta, error) {
	val, err := svcCtx.Redis.Get(ctx, uploadRedisKey(fileID)).Result()
	if err != nil {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "upload meta not found")
	}
	var meta uploadMeta
	if err := json.Unmarshal([]byte(val), &meta); err != nil {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "parse upload meta failed")
	}
	return &meta, nil
}
