package service

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"path"
	"path/filepath"
	"strings"

	"dubbo.apache.org/dubbo-go/v3/common/logger"
	"github.com/dubbogo/grpc-go/metadata"
	filesApi "github.com/filebrowser/filebrowser/v2/api/files"
	fbErrors "github.com/filebrowser/filebrowser/v2/errors"
	"github.com/filebrowser/filebrowser/v2/files"
	"github.com/filebrowser/filebrowser/v2/fileutils"
	"github.com/filebrowser/filebrowser/v2/http"
	"github.com/filebrowser/filebrowser/v2/img"
	"github.com/filebrowser/filebrowser/v2/rules"
	"github.com/filebrowser/filebrowser/v2/search"
	"github.com/mholt/archiver/v3"
	"github.com/samber/lo"
	"github.com/spf13/afero"
)

type FilesProvider struct {
	filesApi.UnimplementedFileServer
}

const BASE_PATH = "../"

var imgSvc = img.New(4) // 图片预览可用协程数

func (f *FilesProvider) List(ctx context.Context, req *filesApi.FileListReq) (*filesApi.FileListResp, error) {
	file, err := files.NewFileInfo(&files.FileOptions{
		Fs:         getFs(req.UserSpacePath),
		Path:       req.Path,
		Modify:     true,
		Expand:     true,
		ReadHeader: true,
		Content:    true,
		Checker:    rules.EmptyChecker,
	})
	if err != nil {
		return nil, err
	}
	if !file.IsDir {
		return nil, errors.New("路径不是文件夹")
	}
	file.Listing.Sorting.Asc = req.Sorting.Asc
	file.Listing.Sorting.By = req.Sorting.By
	file.Listing.ApplySort()
	result := &filesApi.FileListResp{
		NumDirs:  int32(file.NumDirs),
		NumFiles: int32(file.NumFiles),
		Sorting: &filesApi.Sorting{
			By:  file.Sorting.By,
			Asc: file.Sorting.Asc,
		},
		Path:      file.Path,
		Name:      file.Name,
		Size:      file.Size,
		Extension: file.Extension,
		Mode:      file.Mode.String(),
		IsDir:     file.IsDir,
		IsSymlink: file.IsSymlink,
		Type:      file.Type,
		Items:     make([]*filesApi.Items, 0, len(file.Listing.Items)),
	}
	for _, v := range file.Listing.Items {
		result.Items = append(result.Items, &filesApi.Items{
			Path:      v.Path,
			Name:      v.Name,
			Size:      v.Size,
			Extension: v.Extension,
			Mode:      v.Mode.String(),
			IsDir:     v.IsDir,
			IsSymlink: v.IsSymlink,
			Type:      v.Type,
		})
	}
	return result, err
}

func (f *FilesProvider) Create(ctx context.Context, req *filesApi.CreateReq) (*filesApi.CreateResp, error) {
	fs := getFs(req.UserSpacePath)
	if strings.HasSuffix(req.Path, "/") {
		return new(filesApi.CreateResp), fs.MkdirAll(req.Path, files.PermDir)
	}
	_, err := fs.Create(req.Path)
	return new(filesApi.CreateResp), err
}

func (f *FilesProvider) Delete(ctx context.Context, req *filesApi.DeleteReq) (*filesApi.DeleteResp, error) {
	fs := getFs(req.UserSpacePath)
	return new(filesApi.DeleteResp), fs.RemoveAll(req.Path)
}

func (f *FilesProvider) Upload(ctx context.Context, req *filesApi.UploadReq) (*filesApi.UploadResp, error) {
	fs := getFs(req.UserSpacePath)
	fi, err := fs.Create(req.Path)
	if err != nil {
		return nil, err
	}
	defer fi.Close()
	_, err = fi.Write(req.Content)
	if err != nil {
		return nil, err
	}
	return new(filesApi.UploadResp), nil
}

func (f *FilesProvider) Search(ctx context.Context, req *filesApi.SearchReq) (*filesApi.SearchResp, error) {
	fs := getFs(req.UserSpacePath)
	result := new(filesApi.SearchResp)
	err := search.Search(fs, req.Path, req.Query, rules.EmptyChecker, func(path string, f os.FileInfo) error {
		result.Items = append(result.Items, &filesApi.SearchResp_Nested{
			Dir:  f.IsDir(),
			Path: path,
		})

		return nil
	})

	if err != nil {
		return nil, err
	}
	return result, err
}

func (f *FilesProvider) TusCreate(ctx context.Context, req *filesApi.TusCreateReq) (*filesApi.TusCreateResp, error) {
	fs := getFs(req.UserSpacePath)
	file, err := files.NewFileInfo(&files.FileOptions{
		Fs:         fs,
		Path:       req.Path,
		Modify:     true,
		Expand:     false,
		ReadHeader: true,
		Checker:    rules.EmptyChecker,
	})
	switch {
	case errors.Is(err, afero.ErrFileNotFound):
		dirPath := filepath.Dir(req.Path)
		if _, statErr := fs.Stat(dirPath); os.IsNotExist(statErr) {
			if mkdirErr := fs.MkdirAll(dirPath, files.PermDir); mkdirErr != nil {
				return nil, err
			}
		}
	case err != nil:
		return nil, err
	}
	fileFlags := os.O_CREATE | os.O_WRONLY
	if req.Override {
		fileFlags |= os.O_TRUNC
	}

	// if file exists
	if file != nil {
		if file.IsDir {
			return nil, fmt.Errorf("上传路径不能是文件夹 %s", file.RealPath())
		}
	}

	openFile, err := fs.OpenFile(req.Path, fileFlags, files.PermFile)
	if err != nil {
		return nil, err
	}
	if err := openFile.Close(); err != nil {
		return nil, err
	}
	logger.Infof("创建空文件,%v", openFile.Name())
	return &filesApi.TusCreateResp{
		UploadLength: -1,
		UploadOffset: 0,
	}, nil
}

func (f *FilesProvider) TusUpload(ctx context.Context, req *filesApi.TusUploadReq) (*filesApi.TusUploadResp, error) {
	fs := getFs(req.UserSpacePath)
	file, err := files.NewFileInfo(&files.FileOptions{
		Fs:         fs,
		Path:       req.Path,
		Modify:     true,
		Expand:     false,
		ReadHeader: true,
		Checker:    rules.EmptyChecker,
	})
	switch {
	case errors.Is(err, afero.ErrFileNotFound):
		return nil, errors.New("未找到文件")
	case err != nil:
		return nil, err
	}

	switch {
	case file.IsDir:
		return nil, fmt.Errorf("上传路径不能是文件夹 %s", file.RealPath())
	case file.Size != req.UploadOffset:
		return nil, fmt.Errorf(
			"%s 文件大小符合: %d",
			file.RealPath(),
			req.UploadOffset,
		)
	}
	openFile, err := fs.OpenFile(req.Path, os.O_WRONLY|os.O_APPEND, files.PermFile)
	if err != nil {
		return nil, fmt.Errorf("could not open file: %w", err)
	}
	defer openFile.Close()

	_, err = openFile.Seek(req.UploadOffset, 0)
	if err != nil {
		return nil, fmt.Errorf("could not seek file: %w", err)
	}

	logger.Infof("写入文件块,offset:%v,块大小:%vbyte", req.UploadOffset, len(req.Content))
	bytesWritten, err := io.Copy(openFile, bytes.NewBuffer(req.Content))
	if err != nil {
		return nil, fmt.Errorf("could not write to file: %w", err)
	}
	return &filesApi.TusUploadResp{
		UploadOffset: req.UploadOffset + bytesWritten,
	}, nil
}

func (f *FilesProvider) Info(ctx context.Context, req *filesApi.FileInfoReq) (*filesApi.FileInfoResp, error) {
	fs := getFs(req.UserSpacePath)
	file, err := files.NewFileInfo(&files.FileOptions{
		Fs:         fs,
		Path:       req.Path,
		Modify:     true,
		Expand:     false,
		ReadHeader: true,
		Checker:    rules.EmptyChecker,
	})
	switch {
	case errors.Is(err, afero.ErrFileNotFound):
		return nil, errors.New("未找到文件")
	case err != nil:
		return nil, err
	}
	result := &filesApi.FileInfoResp{
		Path:      file.Path,
		Name:      file.Name,
		Size:      file.Size,
		Extension: file.Extension,
		Mode:      file.Mode.String(),
		IsDir:     file.IsDir,
		IsSymlink: file.IsSymlink,
		Type:      file.Type,
	}
	return result, nil
}

func (f *FilesProvider) ResumableTransfer(ctx context.Context, req *filesApi.ResumableTransferReq) (*filesApi.ResumableTransferResp, error) {
	fs := getFs(req.UserSpacePath)
	_, err := files.NewFileInfo(&files.FileOptions{
		Fs:         fs,
		Path:       req.Path,
		Modify:     true,
		Expand:     false,
		ReadHeader: true,
		Checker:    rules.EmptyChecker,
	})
	switch {
	case errors.Is(err, afero.ErrFileNotFound):
		return nil, errors.New("未找到文件")
	case err != nil:
		return nil, err
	}
	openFile, err := fs.Open(req.Path)
	if err != nil {
		return nil, fmt.Errorf("could not open file: %w", err)
	}
	defer openFile.Close()
	logger.Debugf("设置文件读取,offset:%v,length:%v", req.Offset, req.Length)
	_, err = openFile.Seek(req.Offset, 0)
	if err != nil {
		return nil, err
	}
	b := make([]byte, req.Length)
	n, err := openFile.Read(b)
	if err != nil {
		return nil, err
	}
	return &filesApi.ResumableTransferResp{
		Content: b[:n],
	}, nil
}

func (f *FilesProvider) Preview(ctx context.Context, req *filesApi.PreviewReq) (*filesApi.PreviewResp, error) {
	fs := getFs(req.UserSpacePath)
	file, err := files.NewFileInfo(&files.FileOptions{
		Fs:         fs,
		Path:       req.Path,
		Modify:     true,
		Expand:     true,
		ReadHeader: true,
		Checker:    rules.EmptyChecker,
	})
	if err != nil {
		return nil, err
	}
	switch file.Type {
	case "image":
		{
			b, err := f.createPreview(imgSvc, file, req.Size)
			if err != nil {
				return nil, err
			}
			return &filesApi.PreviewResp{
				Content:  b,
				FileName: file.Name,
				ModTime:  file.ModTime.UnixMilli(),
			}, nil

		}
	default:
		return nil, errors.New("目前只支持图片类型的预览")
	}
}

func (f *FilesProvider) createPreview(imgSvc http.ImgService,
	file *files.FileInfo, previewSize uint32) ([]byte, error) {
	fd, err := file.Fs.Open(file.Path)
	if err != nil {
		return nil, err
	}
	defer fd.Close()

	var (
		width   int
		height  int
		options []img.Option
	)

	switch {
	case previewSize == 1:
		width = 1080
		height = 1080
		options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium))
	case previewSize == 0:
		width = 256
		height = 256
		options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg))
	default:
		return nil, img.ErrUnsupportedFormat
	}

	buf := &bytes.Buffer{}
	if err := imgSvc.Resize(context.Background(), fd, width, height, buf, options...); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

func (f *FilesProvider) Action(_ context.Context, req *filesApi.ActionReq) (*filesApi.ActionResp, error) {
	fs := getFs(req.UserSpacePath)
	if req.Destination == "/" || req.Path == "/" {
		return nil, errors.New("禁止操作用户空间根目录")
	}
	err := checkParent(req.Path, req.Destination)
	if err != nil {
		return nil, err
	}
	if !req.Override && !req.Rename {
		if _, err = fs.Stat(req.Destination); err == nil {
			return nil, err
		}
	}
	if req.Rename {
		req.Destination = addVersionSuffix(req.Destination, fs)
	}
	return &filesApi.ActionResp{}, patchAction(req.Action, req.Path, req.Destination, fs)
}

func (f *FilesProvider) DirDownload(req *filesApi.DirDownloadReq, stream filesApi.File_DirDownloadServer) error {
	var filenames []string
	writer := newFileDownloadWriter(stream)
	fs := getFs(req.UserSpacePath)
	file, err := files.NewFileInfo(&files.FileOptions{
		Fs:         fs,
		Path:       req.Path,
		Modify:     true,
		Expand:     false,
		ReadHeader: true,
		Checker:    rules.EmptyChecker,
	})
	if err != nil {
		return err
	}

	if len(req.Files) == 0 {
		filenames = append(filenames, file.Path)
	} else {
		filenames = lo.Map(req.Files, func(name string, _ int) string {
			return filepath.Join(req.Path, slashClean(name))
		})
	}

	extension, ar, err := parseQueryAlgorithm(req.Algo)
	if err != nil {
		return err
	}

	err = ar.Create(writer)
	if err != nil {
		return err
	}
	defer ar.Close()

	commonDir := fileutils.CommonPrefix(filepath.Separator, filenames...)

	name := filepath.Base(commonDir)
	if name == "." || name == "" || name == string(filepath.Separator) {
		name = file.Name
	}
	// Prefix used to distinguish a filelist generated
	// archive from the full directory archive
	if len(filenames) > 1 {
		name = "_" + name
	}
	name += extension
	stream.SendHeader(metadata.MD{
		"filename": []string{name},
	})
	for _, fname := range filenames {
		err = addFile(ar, fs, fname, commonDir)
		if err != nil {
			log.Printf("Failed to archive %s: %v", fname, err)
		}
	}

	return nil
}

type fileDownloadWriter struct { // 用于实现archiver.Writer.Create(out io.Writer) error
	stream filesApi.File_DirDownloadServer
}

func (f *fileDownloadWriter) Write(p []byte) (n int, err error) {
	err = f.stream.Send(&filesApi.DirDownloadResp{
		Content: p,
	})
	if err == nil {
		n = len(p)
	}
	return
}

func newFileDownloadWriter(stream filesApi.File_DirDownloadServer) *fileDownloadWriter {
	return &fileDownloadWriter{
		stream: stream,
	}
}

func getFs(userSpacePath string) afero.Fs {
	bashAbs, _ := filepath.Abs(BASE_PATH)

	if !strings.HasPrefix(userSpacePath, "/") {
		userSpacePath = "/" + userSpacePath
	}
	_, err := os.Stat(bashAbs + userSpacePath)
	if err != nil {
		os.MkdirAll(bashAbs+userSpacePath, os.ModeDir)
	}
	return afero.NewBasePathFs(afero.NewOsFs(), bashAbs+userSpacePath)
}

func checkParent(src, dst string) error {
	rel, err := filepath.Rel(src, dst)
	if err != nil {
		return err
	}

	rel = filepath.ToSlash(rel)
	if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
		return fbErrors.ErrSourceIsParent
	}

	return nil
}

func addVersionSuffix(source string, fs afero.Fs) string {
	counter := 1
	dir, name := path.Split(source)
	ext := filepath.Ext(name)
	base := strings.TrimSuffix(name, ext)

	for {
		if _, err := fs.Stat(source); err != nil {
			break
		}
		renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
		source = path.Join(dir, renamed)
		counter++
	}

	return source
}

func patchAction(action, src, dst string, fs afero.Fs) error {
	switch action {
	case "copy":
		return fileutils.Copy(fs, src, dst)
	case "rename":
		src = path.Clean("/" + src)
		dst = path.Clean("/" + dst)

		_, err := files.NewFileInfo(&files.FileOptions{
			Fs:         fs,
			Path:       src,
			Modify:     true,
			Expand:     false,
			ReadHeader: false,
			Checker:    rules.EmptyChecker,
		})
		if err != nil {
			return err
		}

		return fileutils.MoveFile(fs, src, dst)
	default:
		return fmt.Errorf("unsupported action %s: %w", action, fbErrors.ErrInvalidRequestParams)
	}
}

func slashClean(name string) string {
	if name == "" || name[0] != '/' {
		name = "/" + name
	}
	return path.Clean(name)
}

func parseQueryAlgorithm(algo string) (string, archiver.Writer, error) {
	switch algo {
	case "zip", "true", "":
		return ".zip", archiver.NewZip(), nil
	case "tar":
		return ".tar", archiver.NewTar(), nil
	case "targz":
		return ".tar.gz", archiver.NewTarGz(), nil
	case "tarbz2":
		return ".tar.bz2", archiver.NewTarBz2(), nil
	case "tarxz":
		return ".tar.xz", archiver.NewTarXz(), nil
	case "tarlz4":
		return ".tar.lz4", archiver.NewTarLz4(), nil
	case "tarsz":
		return ".tar.sz", archiver.NewTarSz(), nil
	default:
		return "", nil, errors.New("format not implemented")
	}
}

func addFile(ar archiver.Writer, fs afero.Fs, path, commonPath string) error {
	info, err := fs.Stat(path)
	if err != nil {
		return err
	}

	if !info.IsDir() && !info.Mode().IsRegular() {
		return nil
	}

	file, err := fs.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	if path != commonPath {
		filename := strings.TrimPrefix(path, commonPath)
		filename = strings.TrimPrefix(filename, string(filepath.Separator))
		err = ar.Write(archiver.File{
			FileInfo: archiver.FileInfo{
				FileInfo:   info,
				CustomName: filename,
			},
			ReadCloser: file,
		})
		if err != nil {
			return err
		}
	}

	if info.IsDir() {
		names, err := file.Readdirnames(0)
		if err != nil {
			return err
		}

		for _, name := range names {
			fPath := filepath.Join(path, name)
			err = addFile(ar, fs, fPath, commonPath)
			if err != nil {
				log.Printf("Failed to archive %s: %v", fPath, err)
			}
		}
	}

	return nil
}