package api

import (
	"context"
	"fmt"
	"strconv"

	"github.com/stashapp/stash/internal/manager"
	"github.com/stashapp/stash/pkg/file"
	"github.com/stashapp/stash/pkg/fsutil"
	"github.com/stashapp/stash/pkg/models"
	"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)

func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput) (bool, error) {
	if err := r.withTxn(ctx, func(ctx context.Context) error {
		fileStore := r.repository.File
		folderStore := r.repository.Folder
		mover := file.NewMover(fileStore, folderStore)
		mover.RegisterHooks(ctx)

		var (
			folder   *models.Folder
			basename string
		)

		fileIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
		if err != nil {
			return fmt.Errorf("converting ids: %w", err)
		}

		switch {
		case input.DestinationFolderID != nil:
			var err error

			folderID, err := strconv.Atoi(*input.DestinationFolderID)
			if err != nil {
				return fmt.Errorf("converting destination folder id: %w", err)
			}

			folder, err = folderStore.Find(ctx, models.FolderID(folderID))
			if err != nil {
				return fmt.Errorf("finding destination folder: %w", err)
			}

			if folder == nil {
				return fmt.Errorf("folder with id %d not found", input.DestinationFolderID)
			}

			if folder.ZipFileID != nil {
				return fmt.Errorf("cannot move to %s, is in a zip file", folder.Path)
			}
		case input.DestinationFolder != nil:
			folderPath := *input.DestinationFolder

			// ensure folder path is within the library
			if err := r.validateFolderPath(folderPath); err != nil {
				return err
			}

			// get or create folder hierarchy
			var err error
			folder, err = file.GetOrCreateFolderHierarchy(ctx, folderStore, folderPath)
			if err != nil {
				return fmt.Errorf("getting or creating folder hierarchy: %w", err)
			}
		default:
			return fmt.Errorf("must specify destination folder or path")
		}

		if input.DestinationBasename != nil {
			// ensure only one file was supplied
			if len(input.Ids) != 1 {
				return fmt.Errorf("must specify one file when providing destination path")
			}

			basename = *input.DestinationBasename
		}

		// create the folder hierarchy in the filesystem if needed
		if err := mover.CreateFolderHierarchy(folder.Path); err != nil {
			return fmt.Errorf("creating folder hierarchy %s in filesystem: %w", folder.Path, err)
		}

		for _, fileIDInt := range fileIDs {
			fileID := models.FileID(fileIDInt)
			f, err := fileStore.Find(ctx, fileID)
			if err != nil {
				return fmt.Errorf("finding file %d: %w", fileID, err)
			}

			// ensure that the file extension matches the existing file type
			if basename != "" {
				if err := r.validateFileExtension(f[0].Base().Basename, basename); err != nil {
					return err
				}
			}

			if err := mover.Move(ctx, f[0], folder, basename); err != nil {
				return err
			}
		}

		return nil
	}); err != nil {
		return false, err
	}

	return true, nil
}

func (r *mutationResolver) validateFolderPath(folderPath string) error {
	paths := manager.GetInstance().Config.GetStashPaths()
	if l := paths.GetStashFromDirPath(folderPath); l == nil {
		return fmt.Errorf("folder path %s must be within a stash library path", folderPath)
	}

	return nil
}

func (r *mutationResolver) validateFileExtension(oldBasename, newBasename string) error {
	c := manager.GetInstance().Config
	if err := r.validateFileExtensionList(c.GetVideoExtensions(), oldBasename, newBasename); err != nil {
		return err
	}

	if err := r.validateFileExtensionList(c.GetImageExtensions(), oldBasename, newBasename); err != nil {
		return err
	}

	if err := r.validateFileExtensionList(c.GetGalleryExtensions(), oldBasename, newBasename); err != nil {
		return err
	}

	return nil
}

func (r *mutationResolver) validateFileExtensionList(exts []string, oldBasename, newBasename string) error {
	if fsutil.MatchExtension(oldBasename, exts) && !fsutil.MatchExtension(newBasename, exts) {
		return fmt.Errorf("file extension for %s is inconsistent with old filename %s", newBasename, oldBasename)
	}

	return nil
}

func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret bool, err error) {
	fileIDs, err := stringslice.StringSliceToIntSlice(ids)
	if err != nil {
		return false, fmt.Errorf("converting ids: %w", err)
	}

	fileDeleter := file.NewDeleter()
	destroyer := &file.ZipDestroyer{
		FileDestroyer:   r.repository.File,
		FolderDestroyer: r.repository.Folder,
	}

	if err := r.withTxn(ctx, func(ctx context.Context) error {
		qb := r.repository.File

		for _, fileIDInt := range fileIDs {
			fileID := models.FileID(fileIDInt)
			f, err := qb.Find(ctx, fileID)
			if err != nil {
				return err
			}

			path := f[0].Base().Path

			// ensure not a primary file
			isPrimary, err := qb.IsPrimary(ctx, fileID)
			if err != nil {
				return fmt.Errorf("checking if file %s is primary: %w", path, err)
			}

			if isPrimary {
				return fmt.Errorf("cannot delete primary file %s", path)
			}

			// destroy files in zip file
			inZip, err := qb.FindByZipFileID(ctx, fileID)
			if err != nil {
				return fmt.Errorf("finding zip file contents for %s: %w", path, err)
			}

			for _, ff := range inZip {
				const deleteFileInZip = false
				if err := file.Destroy(ctx, qb, ff, fileDeleter, deleteFileInZip); err != nil {
					return fmt.Errorf("destroying file %s: %w", ff.Base().Path, err)
				}
			}

			const deleteFile = true
			if err := destroyer.DestroyZip(ctx, f[0], fileDeleter, deleteFile); err != nil {
				return fmt.Errorf("deleting file %s: %w", path, err)
			}
		}

		return nil
	}); err != nil {
		fileDeleter.Rollback()
		return false, err
	}

	// perform the post-commit actions
	fileDeleter.Commit()

	return true, nil
}

func (r *mutationResolver) FileSetFingerprints(ctx context.Context, input FileSetFingerprintsInput) (bool, error) {
	fileIDInt, err := strconv.Atoi(input.ID)
	if err != nil {
		return false, fmt.Errorf("converting id: %w", err)
	}

	fileID := models.FileID(fileIDInt)

	// determine what we're doing
	var (
		fingerprints []models.Fingerprint
		toDelete     []string
	)

	for _, i := range input.Fingerprints {
		if i.Type == models.FingerprintTypeMD5 || i.Type == models.FingerprintTypeOshash {
			return false, fmt.Errorf("cannot modify %s fingerprint", i.Type)
		}

		if i.Value == nil {
			toDelete = append(toDelete, i.Type)
		} else {
			// phashes need to be converted from string into uint64
			var v interface{}
			v = *i.Value

			if i.Type == models.FingerprintTypePhash {
				vInt, err := strconv.ParseUint(*i.Value, 16, 64)
				if err != nil {
					return false, fmt.Errorf("converting phash %s: %w", *i.Value, err)
				}

				v = vInt
			}

			fingerprints = append(fingerprints, models.Fingerprint{
				Type:        i.Type,
				Fingerprint: v,
			})
		}
	}

	if err := r.withTxn(ctx, func(ctx context.Context) error {
		qb := r.repository.File

		if len(fingerprints) > 0 {
			if err := qb.ModifyFingerprints(ctx, fileID, fingerprints); err != nil {
				return fmt.Errorf("modifying fingerprints: %w", err)
			}
		}

		if len(toDelete) > 0 {
			if err := qb.DestroyFingerprints(ctx, fileID, toDelete); err != nil {
				return fmt.Errorf("destroying fingerprints: %w", err)
			}
		}

		return nil
	}); err != nil {
		return false, err
	}

	return true, nil
}
