package service

import (
	"bytes"
	"fmt"
	"math"
	"net/url"
	"os"
	"path"
	"strconv"
	"strings"
	"time"

	"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/buserr"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/global"
	"github.com/1Panel-dev/1Panel/agent/utils/common"
	"github.com/1Panel-dev/1Panel/agent/utils/files"
	"github.com/1Panel-dev/1Panel/agent/utils/re"
	"github.com/shirou/gopsutil/v4/disk"
	"gopkg.in/ini.v1"
)

const (
	recycleBinClashDir    = ".1panel_clash"
	recycleBinFilesSubdir = "files"
	recycleBinInfoSubdir  = "info"
	trashInfoSuffix       = ".trashinfo"
	trashInfoSection      = "Trash Info"
	trashInfoTimeLayout   = "2006-01-02T15:04:05"
)

type RecycleBinService struct {
}

type IRecycleBinService interface {
	Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error)
	Create(create request.RecycleBinCreate) error
	Reduce(reduce request.RecycleBinReduce) error
	Clear() error
}

func NewIRecycleBinService() IRecycleBinService {
	return &RecycleBinService{}
}

func (r RecycleBinService) Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error) {
	var result []response.RecycleBinDTO
	partitions, err := disk.Partitions(false)
	if err != nil {
		return 0, nil, err
	}
	op := files.NewFileOp()
	for _, p := range partitions {
		clashRoot := path.Join(p.Mountpoint, recycleBinClashDir)
		if !op.Stat(clashRoot) {
			continue
		}
		result = append(result, collectTrashEntries(clashRoot)...)
		result = append(result, collectLegacyEntries(clashRoot)...)
	}
	if search.PageSize <= 0 {
		return int64(len(result)), []response.RecycleBinDTO{}, nil
	}
	page := search.Page
	if page < 1 {
		page = 1
	}
	startIndex := (page - 1) * search.PageSize
	endIndex := startIndex + search.PageSize

	if startIndex >= len(result) {
		return int64(len(result)), []response.RecycleBinDTO{}, nil
	}
	if endIndex > len(result) {
		endIndex = len(result)
	}
	return int64(len(result)), result[startIndex:endIndex], nil
}

func (r RecycleBinService) Create(create request.RecycleBinCreate) error {
	if files.IsProtected(create.SourcePath) {
		return buserr.New("ErrPathNotDelete")
	}
	op := files.NewFileOp()
	if !op.Stat(create.SourcePath) {
		return buserr.New("ErrLinkPathNotFound")
	}
	clashRoot, err := getClashDir(create.SourcePath)
	if err != nil {
		return err
	}
	filesDir, infoDir, err := ensureTrashDirs(clashRoot)
	if err != nil {
		return err
	}

	deleteTime := time.Now()
	openFile, err := op.OpenFile(create.SourcePath)
	if err != nil {
		return err
	}
	fileInfo, err := openFile.Stat()
	_ = openFile.Close()
	if err != nil {
		return err
	}
	size := int64(0)
	if fileInfo.IsDir() {
		sizeF, err := op.GetDirSize(create.SourcePath)
		if err != nil {
			return err
		}
		size = int64(sizeF)
	} else {
		size = fileInfo.Size()
	}

	rName := allocateTrashEntryName(filesDir, infoDir, create.SourcePath)
	info := trashInfo{
		Path:         create.SourcePath,
		DeletionDate: deleteTime,
		Size:         size,
		IsDir:        fileInfo.IsDir(),
	}
	infoPath := path.Join(infoDir, rName+trashInfoSuffix)
	if err := writeTrashInfo(infoPath, info); err != nil {
		return err
	}
	targetPath := path.Join(filesDir, rName)
	if err := op.Mv(create.SourcePath, targetPath); err != nil {
		if rmErr := os.Remove(infoPath); rmErr != nil && !os.IsNotExist(rmErr) {
			global.LOG.Warnf("rollback trashinfo failed for %s: %v", infoPath, rmErr)
		}
		return err
	}
	return nil
}

func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error {
	filePath := path.Join(reduce.From, reduce.RName)
	op := files.NewFileOp()
	if !op.Stat(filePath) {
		return buserr.New("ErrLinkPathNotFound")
	}
	recycleBinDTO, err := loadRecycleBinDTO(reduce.From, reduce.RName)
	if err != nil {
		return err
	}
	if recycleBinDTO.SourcePath == "" {
		return buserr.New("ErrSourcePathNotFound")
	}
	if !op.Stat(path.Dir(recycleBinDTO.SourcePath)) {
		return buserr.New("ErrSourcePathNotFound")
	}
	if op.Stat(recycleBinDTO.SourcePath) {
		if err = op.RmRf(recycleBinDTO.SourcePath); err != nil {
			return err
		}
	}
	if err := op.Mv(filePath, recycleBinDTO.SourcePath); err != nil {
		return err
	}
	if err := cleanupTrashInfoByEntryPath(filePath); err != nil {
		global.LOG.Warnf("cleanup trashinfo failed for %s: %v", filePath, err)
	}
	return nil
}

func (r RecycleBinService) Clear() error {
	partitions, err := disk.Partitions(false)
	if err != nil {
		return err
	}
	op := files.NewFileOp()
	for _, p := range partitions {
		dir := path.Join(p.Mountpoint, recycleBinClashDir)
		if !op.Stat(dir) {
			continue
		}
		newDir := path.Join(p.Mountpoint, recycleBinClashDir+"-"+strings.ReplaceAll(common.GetUuid(), "-", ""))
		if err := op.Mv(dir, newDir); err != nil {
			return err
		}
		go func(target string) {
			defer func() {
				if r := recover(); r != nil {
					global.LOG.Warnf("clear recycle bin panic on %s: %v", target, r)
				}
			}()
			_ = op.DeleteDir(target)
		}(newDir)
	}
	return nil
}

func getClashDir(realPath string) (string, error) {
	partitions, err := disk.Partitions(false)
	if err != nil {
		return "", err
	}
	for _, p := range partitions {
		if p.Mountpoint == "/" {
			continue
		}
		if strings.HasPrefix(realPath, p.Mountpoint) {
			clashDir := path.Join(p.Mountpoint, recycleBinClashDir)
			if err = createClashDir(clashDir); err != nil {
				return "", err
			}
			return clashDir, nil
		}
	}
	return global.Dir.RecycleBinDir, createClashDir(global.Dir.RecycleBinDir)
}

func createClashDir(clashDir string) error {
	op := files.NewFileOp()
	if !op.Stat(clashDir) {
		if err := op.CreateDir(clashDir, constant.DirPerm); err != nil {
			return err
		}
	}
	return nil
}

func ensureTrashDirs(clashRoot string) (string, string, error) {
	filesDir := path.Join(clashRoot, recycleBinFilesSubdir)
	infoDir := path.Join(clashRoot, recycleBinInfoSubdir)
	op := files.NewFileOp()
	if !op.Stat(filesDir) {
		if err := op.CreateDir(filesDir, constant.DirPerm); err != nil {
			return "", "", err
		}
	}
	if !op.Stat(infoDir) {
		if err := op.CreateDir(infoDir, constant.DirPerm); err != nil {
			return "", "", err
		}
	}
	return filesDir, infoDir, nil
}

func trashFilesDir(clashRoot string) string {
	return path.Join(clashRoot, recycleBinFilesSubdir)
}

func trashInfoDir(clashRoot string) string {
	return path.Join(clashRoot, recycleBinInfoSubdir)
}

func trashInfoPath(infoDir, entryName string) string {
	return path.Join(infoDir, entryName+trashInfoSuffix)
}

type trashInfo struct {
	Path         string
	DeletionDate time.Time
	Size         int64
	IsDir        bool
}

func writeTrashInfo(dstPath string, info trashInfo) error {
	cfg := ini.Empty()
	section, err := cfg.NewSection(trashInfoSection)
	if err != nil {
		return err
	}
	if _, err := section.NewKey("Path", url.PathEscape(info.Path)); err != nil {
		return err
	}
	if _, err := section.NewKey("DeletionDate", info.DeletionDate.Format(trashInfoTimeLayout)); err != nil {
		return err
	}
	if _, err := section.NewKey("Size", strconv.FormatInt(info.Size, 10)); err != nil {
		return err
	}
	if _, err := section.NewKey("IsDir", strconv.FormatBool(info.IsDir)); err != nil {
		return err
	}

	var buf bytes.Buffer
	if _, err := cfg.WriteTo(&buf); err != nil {
		return err
	}
	tmp := dstPath + ".tmp"
	if err := os.WriteFile(tmp, buf.Bytes(), constant.FilePerm); err != nil {
		return err
	}
	return os.Rename(tmp, dstPath)
}

func readTrashInfo(srcPath string) (*trashInfo, error) {
	cfg, err := ini.Load(srcPath)
	if err != nil {
		return nil, err
	}
	section, err := cfg.GetSection(trashInfoSection)
	if err != nil {
		return nil, err
	}
	rawPath := section.Key("Path").Value()
	decodedPath, err := url.PathUnescape(rawPath)
	if err != nil {
		decodedPath = rawPath
	}
	info := &trashInfo{
		Path:  decodedPath,
		IsDir: section.Key("IsDir").MustBool(false),
	}
	if deletionStr := section.Key("DeletionDate").Value(); deletionStr != "" {
		if t, parseErr := time.ParseInLocation(trashInfoTimeLayout, deletionStr, time.Local); parseErr == nil {
			info.DeletionDate = t
		}
	}
	if sizeStr := section.Key("Size").Value(); sizeStr != "" {
		if sz, sizeErr := strconv.ParseInt(sizeStr, 10, 64); sizeErr == nil {
			info.Size = sz
		}
	}
	return info, nil
}

func collectTrashEntries(clashRoot string) []response.RecycleBinDTO {
	filesDir := trashFilesDir(clashRoot)
	infoDir := trashInfoDir(clashRoot)
	op := files.NewFileOp()
	if !op.Stat(filesDir) {
		return nil
	}
	entries, err := os.ReadDir(filesDir)
	if err != nil {
		global.LOG.Warnf("read recycle files dir %s failed: %v", filesDir, err)
		return nil
	}
	var result []response.RecycleBinDTO
	for _, entry := range entries {
		dto, err := buildTrashDTO(filesDir, infoDir, entry)
		if err != nil {
			global.LOG.Warnf("build recycle dto for %s failed: %v", entry.Name(), err)
			continue
		}
		result = append(result, *dto)
	}
	return result
}

func collectLegacyEntries(clashRoot string) []response.RecycleBinDTO {
	op := files.NewFileOp()
	if !op.Stat(clashRoot) {
		return nil
	}
	entries, err := os.ReadDir(clashRoot)
	if err != nil {
		return nil
	}
	var result []response.RecycleBinDTO
	for _, entry := range entries {
		name := entry.Name()
		if name == recycleBinFilesSubdir || name == recycleBinInfoSubdir {
			continue
		}
		if !strings.HasPrefix(name, "_1p_") {
			continue
		}
		dto, err := getRecycleBinDTOFromName(name)
		if err != nil {
			continue
		}
		dto.IsDir = entry.IsDir()
		dto.From = clashRoot
		result = append(result, *dto)
	}
	return result
}

func buildTrashDTO(filesDir, infoDir string, entry os.DirEntry) (*response.RecycleBinDTO, error) {
	entryName := entry.Name()
	infoPath := trashInfoPath(infoDir, entryName)
	if info, err := readTrashInfo(infoPath); err == nil {
		return trashInfoToDTO(entryName, filesDir, info), nil
	} else if !os.IsNotExist(err) {
		global.LOG.Warnf("read trashinfo %s failed: %v", infoPath, err)
	}

	entryPath := path.Join(filesDir, entryName)
	fi, err := os.Stat(entryPath)
	if err != nil {
		return nil, err
	}
	size := fi.Size()
	if fi.IsDir() {
		if sz, sizeErr := files.NewFileOp().GetDirSize(entryPath); sizeErr == nil {
			size = int64(sz)
		}
	}
	return &response.RecycleBinDTO{
		Name:       entryName,
		Size:       clampToInt(size),
		Type:       "file",
		DeleteTime: fi.ModTime(),
		RName:      entryName,
		SourcePath: "",
		IsDir:      fi.IsDir(),
		From:       filesDir,
	}, nil
}

func trashInfoToDTO(entryName, filesDir string, info *trashInfo) *response.RecycleBinDTO {
	return &response.RecycleBinDTO{
		Name:       path.Base(info.Path),
		Size:       clampToInt(info.Size),
		Type:       "file",
		DeleteTime: info.DeletionDate,
		RName:      entryName,
		SourcePath: info.Path,
		IsDir:      info.IsDir,
		From:       filesDir,
	}
}

func loadRecycleBinDTO(from, name string) (*response.RecycleBinDTO, error) {
	if path.Base(from) == recycleBinFilesSubdir {
		infoDir := trashInfoDir(path.Dir(from))
		info, err := readTrashInfo(trashInfoPath(infoDir, name))
		if err == nil {
			return trashInfoToDTO(name, from, info), nil
		}
		if !os.IsNotExist(err) {
			global.LOG.Warnf("read trashinfo for %s failed: %v", name, err)
		}
	}

	dto, err := getRecycleBinDTOFromName(name)
	if err != nil {
		return nil, err
	}
	dto.From = from
	return dto, nil
}

func cleanupTrashInfoByEntryPath(filePath string) error {
	cleaned := path.Clean(filePath)
	parent := path.Dir(cleaned)
	if path.Base(parent) != recycleBinFilesSubdir {
		return nil
	}
	clashRoot := path.Dir(parent)
	if path.Base(clashRoot) != recycleBinClashDir {
		return nil
	}
	infoPath := trashInfoPath(trashInfoDir(clashRoot), path.Base(cleaned))
	if err := os.Remove(infoPath); err != nil && !os.IsNotExist(err) {
		return err
	}
	return nil
}

func allocateTrashEntryName(filesDir, infoDir, sourcePath string) string {
	ext := path.Ext(path.Base(sourcePath))
	op := files.NewFileOp()
	for i := 0; i < 5; i++ {
		name := strings.ReplaceAll(common.GetUuid(), "-", "") + ext
		if op.Stat(path.Join(filesDir, name)) {
			continue
		}
		if op.Stat(trashInfoPath(infoDir, name)) {
			continue
		}
		return name
	}
	return fmt.Sprintf("%s-%d%s", strings.ReplaceAll(common.GetUuid(), "-", ""), time.Now().UnixNano(), ext)
}

func clampToInt(v int64) int {
	if v > math.MaxInt {
		return math.MaxInt
	}
	if v < math.MinInt {
		return math.MinInt
	}
	return int(v)
}

func getRecycleBinDTOFromName(filename string) (*response.RecycleBinDTO, error) {
	matches := re.GetRegex(re.RecycleBinFilePattern).FindStringSubmatch(filename)
	if len(matches) != 4 {
		return nil, fmt.Errorf("invalid filename format")
	}
	sourcePath := "/" + strings.ReplaceAll(matches[1], "_1p_", "/")
	size, err := strconv.ParseInt(matches[2], 10, 64)
	if err != nil {
		return nil, err
	}
	if size < math.MinInt || size > math.MaxInt {
		return nil, fmt.Errorf("size out of int range")
	}

	deleteTime, err := strconv.ParseInt(matches[3], 10, 64)
	if err != nil {
		return nil, err
	}
	return &response.RecycleBinDTO{
		Name:       path.Base(sourcePath),
		Size:       int(size),
		Type:       "file",
		DeleteTime: time.Unix(deleteTime, 0),
		SourcePath: sourcePath,
		RName:      filename,
	}, nil
}
