package manager

import (
	"archive/zip"
	"context"
	"fmt"
	"io"
	"path"
	"path/filepath"
	"strings"

	"github.com/cloudreve/Cloudreve/v4/inventory/types"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
	"golang.org/x/tools/container/intsets"
)

func (m *manager) CreateArchive(ctx context.Context, uris []*fs.URI, writer io.Writer, opts ...fs.Option) (int, error) {
	o := newOption()
	for _, opt := range opts {
		opt.Apply(o)
	}

	failed := 0

	// List all top level files
	files := make([]fs.File, 0, len(uris))
	for _, uri := range uris {
		file, err := m.Get(ctx, uri, dbfs.WithFileEntities(), dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityDownloadFile), dbfs.WithNotRoot())
		if err != nil {
			return 0, fmt.Errorf("failed to get file %s: %w", uri, err)
		}

		files = append(files, file)
	}

	zipWriter := zip.NewWriter(writer)
	defer zipWriter.Close()

	var compressed int64
	for _, file := range files {
		if file.Type() == types.FileTypeFile {
			if err := m.compressFileToArchive(ctx, "/", file, zipWriter, o.ArchiveCompression, o.DryRun); err != nil {
				failed++
				m.l.Warning("Failed to compress file %s: %s, skipping it...", file.Uri(false), err)
			}

			compressed += file.Size()
			if o.ProgressFunc != nil {
				o.ProgressFunc(compressed, file.Size(), 0)
			}

			if o.MaxArchiveSize > 0 && compressed > o.MaxArchiveSize {
				return 0, fs.ErrArchiveSrcSizeTooBig
			}

		} else {
			if err := m.Walk(ctx, file.Uri(false), intsets.MaxInt, func(f fs.File, level int) error {
				if f.Type() == types.FileTypeFolder || f.IsSymbolic() {
					return nil
				}
				if err := m.compressFileToArchive(ctx, strings.TrimPrefix(f.Uri(false).Dir(),
					file.Uri(false).Dir()), f, zipWriter, o.ArchiveCompression, o.DryRun); err != nil {
					failed++
					m.l.Warning("Failed to compress file %s: %s, skipping it...", f.Uri(false), err)
				}

				compressed += f.Size()
				if o.ProgressFunc != nil {
					o.ProgressFunc(compressed, f.Size(), 0)
				}

				if o.MaxArchiveSize > 0 && compressed > o.MaxArchiveSize {
					return fs.ErrArchiveSrcSizeTooBig
				}

				return nil
			}); err != nil {
				m.l.Warning("Failed to walk folder %s: %s, skipping it...", file.Uri(false), err)
				failed++
			}
		}
	}

	return failed, nil
}

func (m *manager) compressFileToArchive(ctx context.Context, parent string, file fs.File, zipWriter *zip.Writer,
	compression bool, dryrun fs.CreateArchiveDryRunFunc) error {
	es, err := m.GetEntitySource(ctx, file.PrimaryEntityID())
	if err != nil {
		return fmt.Errorf("failed to get entity source for file %s: %w", file.Uri(false), err)
	}

	zipName := filepath.FromSlash(path.Join(parent, file.DisplayName()))
	if dryrun != nil {
		dryrun(zipName, es.Entity())
		return nil
	}

	m.l.Debug("Compressing %s to archive...", file.Uri(false))
	header := &zip.FileHeader{
		Name:               zipName,
		Modified:           file.UpdatedAt(),
		UncompressedSize64: uint64(file.Size()),
	}

	if !compression {
		header.Method = zip.Store
	} else {
		header.Method = zip.Deflate
	}

	writer, err := zipWriter.CreateHeader(header)
	if err != nil {
		return fmt.Errorf("failed to create zip header for %s: %w", file.Uri(false), err)
	}

	es.Apply(entitysource.WithContext(ctx))
	_, err = io.Copy(writer, es)
	return err

}
