package service

import (
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"errors"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/dto/request"
	"github.com/1Panel-dev/1Panel/agent/app/dto/response"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/1Panel-dev/1Panel/agent/app/repo"
	"github.com/1Panel-dev/1Panel/agent/buserr"
	"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
	"github.com/1Panel-dev/1Panel/agent/utils/files"
	"gorm.io/gorm"
)

type FileShareService struct{}

const (
	fileShareCodeMinLength     = 10
	fileShareCodeMaxLength     = 16
	fileShareCodeDefaultLength = 13
	fileShareCharset           = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
)

var fileShareCodeRegexp = regexp.MustCompile(`^[A-Za-z0-9]{10,16}$`)

type IFileShareService interface {
	Create(req request.FileShareCreate) (*response.FileShareInfo, error)
	Page(req dto.PageInfo) (int64, []response.FileShareInfo, error)
	GetByPath(path string) (*response.FileShareInfo, error)
	GetByCode(code string) (*response.FileShareInfo, error)
	GetPublicByCode(code string) (*response.FileSharePublicInfo, error)
	DeleteByPath(path string) error
	SharePathCodeMap() (map[string]string, error)
	Check(code, password string) error
	PrepareDownload(code, password string) (filePath, fileName string, err error)
}

func NewIFileShareService() IFileShareService {
	return &FileShareService{}
}

func randomShareCode(length int) (string, error) {
	if length < fileShareCodeMinLength || length > fileShareCodeMaxLength {
		length = fileShareCodeDefaultLength
	}
	b := make([]byte, length)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	buf := make([]byte, length)
	for i := range b {
		buf[i] = fileShareCharset[int(b[i])%len(fileShareCharset)]
	}
	return string(buf), nil
}

func randomSalt() (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}

func hashPassword(salt, password string) string {
	sum := sha256.Sum256([]byte(salt + ":" + password))
	return hex.EncodeToString(sum[:])
}

func shareModelToInfo(item model.FileShare) response.FileShareInfo {
	return response.FileShareInfo{
		Code:        item.Token,
		Path:        item.Path,
		FileName:    item.FileName,
		ExpiresAt:   item.ExpiresUnix,
		Permanent:   item.ExpiresUnix == 0,
		HasPassword: item.PasswordHash != "",
	}
}

func fillSharePassword(info *response.FileShareInfo, item model.FileShare) {
	if info == nil || item.PasswordEnc == "" {
		return
	}
	password, err := encrypt.StringDecrypt(item.PasswordEnc)
	if err != nil {
		return
	}
	info.Password = password
}

func shareModelToPublicInfo(item model.FileShare) response.FileSharePublicInfo {
	return response.FileSharePublicInfo{
		FileName:    item.FileName,
		ExpiresAt:   item.ExpiresUnix,
		Permanent:   item.ExpiresUnix == 0,
		HasPassword: item.PasswordHash != "",
	}
}

func (s *FileShareService) generateUniqueCode() (string, error) {
	for i := 0; i < 8; i++ {
		code, err := randomShareCode(fileShareCodeDefaultLength)
		if err != nil {
			return "", err
		}
		_, err = fileShareRepo.GetFirst(fileShareRepo.WithByCode(code))
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return code, nil
		}
		if err != nil {
			return "", err
		}
	}
	return "", errors.New("failed to generate unique file share code")
}

func (s *FileShareService) Create(req request.FileShareCreate) (*response.FileShareInfo, error) {
	path := strings.TrimSpace(req.Path)
	if path == "" || strings.Contains(path, "..") {
		return nil, buserr.New("ErrFileSharePath")
	}
	if files.ShouldDenySensitiveFileRead(path) {
		return nil, buserr.New("ErrSensitiveFileRead")
	}
	info, err := os.Stat(path)
	if err != nil || info.IsDir() {
		return nil, buserr.New("ErrFileSharePath")
	}

	item, err := fileShareRepo.GetFirst(fileShareRepo.WithByPath(path))
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	isNew := errors.Is(err, gorm.ErrRecordNotFound)
	if isNew {
		code, err := s.generateUniqueCode()
		if err != nil {
			return nil, err
		}
		item = model.FileShare{
			Path:     path,
			Token:    code,
			FileName: filepath.Base(path),
		}
	} else if !fileShareCodeRegexp.MatchString(item.Token) {
		code, err := s.generateUniqueCode()
		if err != nil {
			return nil, err
		}
		item.Token = code
	}

	item.FileName = filepath.Base(path)
	item.MaxDownloads = 0
	item.DownloadCount = 0
	item.ExpiresUnix = 0
	if req.ExpireMinutes > 0 {
		item.ExpiresUnix = time.Now().Add(time.Duration(req.ExpireMinutes) * time.Minute).Unix()
	}

	if req.Password != nil {
		pw := strings.TrimSpace(*req.Password)
		if pw == "" {
			item.PasswordEnc = ""
			item.PasswordSalt = ""
			item.PasswordHash = ""
		} else {
			pwLen := utf8.RuneCountInString(pw)
			if pwLen < 4 || pwLen > 256 {
				return nil, buserr.New("ErrFileSharePasswordPolicy")
			}
			enc, err := encrypt.StringEncrypt(pw)
			if err != nil {
				return nil, err
			}
			item.PasswordEnc = enc
			salt, err := randomSalt()
			if err != nil {
				return nil, err
			}
			item.PasswordSalt = salt
			item.PasswordHash = hashPassword(salt, pw)
		}
	}

	if isNew {
		if err := fileShareRepo.Create(&item); err != nil {
			return nil, err
		}
	} else {
		if err := fileShareRepo.Save(&item); err != nil {
			return nil, err
		}
	}

	res := shareModelToInfo(item)
	fillSharePassword(&res, item)
	return &res, nil
}

func (s *FileShareService) Page(req dto.PageInfo) (int64, []response.FileShareInfo, error) {
	items, err := fileShareRepo.All()
	if err != nil {
		return 0, nil, err
	}
	result := make([]response.FileShareInfo, 0, len(items))
	for _, item := range items {
		if err := s.pruneInvalidShare(item); err != nil {
			return 0, nil, err
		}
		if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
			continue
		}
		result = append(result, shareModelToInfo(item))
	}
	total := len(result)
	start := (req.Page - 1) * req.PageSize
	if start >= total {
		return int64(total), []response.FileShareInfo{}, nil
	}
	end := start + req.PageSize
	if end > total {
		end = total
	}
	return int64(total), result[start:end], nil
}

func (s *FileShareService) GetByPath(path string) (*response.FileShareInfo, error) {
	item, err := fileShareRepo.GetFirst(fileShareRepo.WithByPath(strings.TrimSpace(path)))
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	if err := s.pruneInvalidShare(item); err != nil {
		return nil, err
	}
	if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
		return nil, nil
	}
	info := shareModelToInfo(item)
	fillSharePassword(&info, item)
	return &info, nil
}

func (s *FileShareService) GetByCode(code string) (*response.FileShareInfo, error) {
	item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code)))
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, buserr.New("ErrFileShareInvalid")
		}
		return nil, err
	}
	if err := s.pruneInvalidShare(item); err != nil {
		return nil, err
	}
	if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
		return nil, buserr.New("ErrFileShareExpired")
	}
	info := shareModelToInfo(item)
	return &info, nil
}

func (s *FileShareService) GetPublicByCode(code string) (*response.FileSharePublicInfo, error) {
	item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code)))
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, buserr.New("ErrFileShareInvalid")
		}
		return nil, err
	}
	if err := s.pruneInvalidShare(item); err != nil {
		return nil, err
	}
	if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
		return nil, buserr.New("ErrFileShareExpired")
	}
	info := shareModelToPublicInfo(item)
	return &info, nil
}

func (s *FileShareService) DeleteByPath(path string) error {
	item, err := fileShareRepo.GetFirst(fileShareRepo.WithByPath(strings.TrimSpace(path)))
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return buserr.New("ErrFileShareInvalid")
		}
		return err
	}
	return fileShareRepo.Delete(repo.WithByID(item.ID))
}

func (s *FileShareService) SharePathCodeMap() (map[string]string, error) {
	items, err := fileShareRepo.All()
	if err != nil {
		return nil, err
	}
	result := make(map[string]string, len(items))
	now := time.Now().Unix()
	for _, item := range items {
		if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
			continue
		}
		if _, err := os.Stat(item.Path); err != nil {
			continue
		}
		result[item.Path] = item.Token
	}
	return result, nil
}

func (s *FileShareService) Check(code, password string) error {
	_, err := s.check(code, password)
	return err
}

func (s *FileShareService) PrepareDownload(code, password string) (string, string, error) {
	item, err := s.check(code, password)
	if err != nil {
		return "", "", err
	}
	return item.Path, item.FileName, nil
}

func (s *FileShareService) pruneInvalidShare(item model.FileShare) error {
	now := time.Now().Unix()
	if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
		return fileShareRepo.Delete(repo.WithByID(item.ID))
	}
	info, err := os.Stat(item.Path)
	if err != nil || info.IsDir() {
		return fileShareRepo.Delete(repo.WithByID(item.ID))
	}
	return nil
}

func (s *FileShareService) check(code, password string) (*model.FileShare, error) {
	code = strings.TrimSpace(code)
	password = strings.TrimSpace(password)
	if code == "" {
		return nil, buserr.New("ErrFileShareInvalid")
	}

	item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(code))
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, buserr.New("ErrFileShareInvalid")
		}
		return nil, err
	}

	now := time.Now().Unix()
	if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
		_ = fileShareRepo.Delete(repo.WithByID(item.ID))
		return nil, buserr.New("ErrFileShareExpired")
	}
	if item.PasswordHash != "" {
		if subtle.ConstantTimeCompare([]byte(hashPassword(item.PasswordSalt, password)), []byte(item.PasswordHash)) != 1 {
			return nil, buserr.New("ErrFileSharePassword")
		}
	}

	info, err := os.Stat(item.Path)
	if err != nil || info.IsDir() {
		_ = fileShareRepo.Delete(repo.WithByID(item.ID))
		return nil, buserr.New("ErrFileSharePath")
	}

	return &item, nil
}
