package storage

import (
	"context"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/tech/sendico/pkg/api/http/response"
	"github.com/tech/sendico/pkg/domainprovider"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	"github.com/tech/sendico/pkg/mutil/fr"
	"github.com/tech/sendico/server/internal/server/fileserviceimp/storage/config"
	"go.uber.org/zap"
)

type LocalStorage struct {
	logger     mlogger.Logger
	storageDir string
	subDir     string
	directory  string
	dp         domainprovider.DomainProvider
	service    mservice.Type
}

func (storage *LocalStorage) Delete(ctx context.Context, objID string) error {
	// Check if context is cancelled
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	filePath, err := storage.resolvePath(objID)
	if err != nil {
		return err
	}
	if err := os.Remove(filePath); err != nil {
		if os.IsNotExist(err) {
			storage.logger.Debug("File not found", zap.String("obj_ref", objID))
			return merrors.NoData("file_not_found")
		}
		storage.logger.Warn("Error occurred while accesing file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objID))
		return err
	}
	return nil
}

func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID string) (string, error) {
	// Check if context is cancelled
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	default:
	}

	filePath, err := storage.resolvePath(objID)
	if err != nil {
		return "", err
	}
	//nolint:gosec // File path is resolved and constrained to storage root.
	dst, err := os.Create(filePath)
	if err != nil {
		storage.logger.Warn("Error occurred while creating file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objID))
		return "", err
	}
	defer fr.CloseFile(storage.logger, dst)

	// Use a goroutine to copy the file and monitor context cancellation
	errCh := make(chan error, 1)
	go func() {
		_, err := io.Copy(dst, file)
		errCh <- err
	}()

	// Wait for either completion or context cancellation
	select {
	case err := <-errCh:
		if err != nil {
			storage.logger.Warn("Error occurred while saving file", zap.Error(err), zap.String("obj_ref", objID))
			return "", err
		}
	case <-ctx.Done():
		// Context was cancelled, clean up the partial file
		if removeErr := os.Remove(filePath); removeErr != nil && !os.IsNotExist(removeErr) {
			storage.logger.Warn("Failed to remove partially written file", zap.Error(removeErr), zap.String("obj_ref", objID))
		}
		return "", ctx.Err()
	}

	return storage.dp.GetAPILink(storage.directory, storage.subDir, objID)
}

func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.HandlerFunc {
	// Check if context is cancelled
	select {
	case <-ctx.Done():
		return response.Internal(storage.logger, storage.service, ctx.Err())
	default:
	}

	filePath, err := storage.resolvePath(objRef)
	if err != nil {
		return response.Internal(storage.logger, storage.service, err)
	}
	if _, err := os.Stat(filePath); err != nil {
		storage.logger.Warn("Failed to access file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objRef))
		return response.Internal(storage.logger, storage.service, err)
	}

	res := func(w http.ResponseWriter, r *http.Request) {
		// Check if the request context is cancelled
		select {
		case <-r.Context().Done():
			storage.logger.Warn("Request canceleed", zap.Error(r.Context().Err()), zap.String("obj_ref", objRef))
			http.Error(w, "Request cancelled", http.StatusRequestTimeout)
			return
		default:
		}
		http.ServeFile(w, r, filePath)
	}

	return res
}

func ensureDir(dirName string) error {
	info, err := os.Stat(dirName)
	if os.IsNotExist(err) {
		return os.MkdirAll(dirName, 0o750)
	}
	if err != nil {
		return err
	}
	if !info.IsDir() {
		return &os.PathError{Op: "mkdir", Path: dirName, Err: os.ErrExist}
	}
	return nil
}

func (storage *LocalStorage) resolvePath(objID string) (string, error) {
	objID = strings.TrimSpace(objID)
	if objID == "" {
		return "", merrors.InvalidArgument("obj_ref is required", "obj_ref")
	}

	filePath := filepath.Join(storage.storageDir, objID)
	relPath, err := filepath.Rel(storage.storageDir, filePath)
	if err != nil {
		return "", merrors.InternalWrap(err, "failed to resolve local file path")
	}
	if relPath == "." || strings.HasPrefix(relPath, "..") {
		return "", merrors.InvalidArgument("obj_ref is invalid", "obj_ref")
	}

	return filePath, nil
}

func CreateLocalFileStorage(logger mlogger.Logger, service mservice.Type, directory, subDir string, dp domainprovider.DomainProvider, cfg config.LocalFSSConfig) (*LocalStorage, error) {
	dir := filepath.Join(cfg.RootPath, directory)
	if err := ensureDir(dir); err != nil {
		logger.Warn("Failed to check directory availability", zap.Error(err), zap.String("dir", dir))
		return nil, err
	}
	res := &LocalStorage{
		logger:     logger.Named("lfs").Named(directory),
		storageDir: dir,
		directory:  directory,
		subDir:     subDir,
		dp:         dp,
		service:    service,
	}
	res.logger.Info("Storage installed", zap.String("root_path", cfg.RootPath), zap.String("directory", directory))
	return res, nil
}
