package component

import (
	"context"
	"fmt"
	"io"
	"path/filepath"

	"github.com/FAU-CDI/wisski-distillery/internal/models"
	"github.com/pkg/errors"
	"github.com/tkw1536/pkglib/fsx/umaskfree"
)

// Backupable represents a component with a Backup method
type Backupable interface {
	Component

	// BackupName returns a new name to be used as an argument for path.
	BackupName() string

	// Backup backs up this component into the destination path path
	Backup(context *StagingContext) error
}

// Snapshotable represents a component with a Snapshot method.
type Snapshotable interface {
	Component

	// SnapshotNeedsRunning returns if this Snapshotable requires a running instance.
	SnapshotNeedsRunning() bool

	// SnapshotName returns a new name to be used as an argument for path.
	SnapshotName() string

	// Snapshot snapshots a part of the instance
	Snapshot(wisski models.Instance, context *StagingContext) error
}

// NewStagingContext returns a new [StagingContext]
func NewStagingContext(ctx context.Context, progress io.Writer, path string, manifest chan<- string) *StagingContext {
	return &StagingContext{
		ctx:      ctx,
		progress: progress,
		path:     path,
		manifest: manifest,
	}
}

// StagingContext is a context used for [Backupable] and [Snapshotable]
type StagingContext struct {
	ctx      context.Context
	progress io.Writer     // writer to direct progress to
	path     string        // path to send files to
	manifest chan<- string // channel the manifest is sent to
}

func (bc *StagingContext) sendPath(path string) {
	// ensure path is absolute!
	if !filepath.IsAbs(path) {
		var err error
		path, err = bc.resolve(path)
		if err != nil {
			fmt.Fprintf(bc.progress, "path resolve error: %s", err)
			return
		}
	}

	// use the relative path for logging
	rel, err := bc.relativize(path)
	if err == nil {
		io.WriteString(bc.progress, rel+"\n")
	}

	// send the absolute path
	bc.manifest <- path
}

// Progress returns a writer to write progress information to.
func (bc *StagingContext) Progress() io.Writer {
	return bc.progress
}

var (
	errResolveAbsolute  = errors.New("resolve: path must be relative")
	errRelativeRelative = errors.New("relativize: path already relative")
)

func (bc *StagingContext) resolve(path string) (dest string, err error) {
	if path == "" {
		return bc.path, nil
	}
	if filepath.IsAbs(path) {
		return "", errResolveAbsolute
	}
	return filepath.Join(bc.path, path), nil
}

func (bc *StagingContext) relativize(path string) (dest string, err error) {
	if !filepath.IsAbs(path) {
		return "", errRelativeRelative
	}

	return filepath.Rel(bc.path, path)
}

// AddDirectory creates a new directory inside the destination.
// Passing the empty path creates the destination as a directory.
//
// It then allows op to fill the file.
func (sc *StagingContext) AddDirectory(path string, op func(context.Context) error) error {
	// check if we are already done
	if err, ok := sc.ctxdone(); ok {
		return err
	}

	// resolve the path!
	dst, err := sc.resolve(path)
	if err != nil {
		return err
	}

	// run the make directory
	if err := umaskfree.Mkdir(dst, umaskfree.DefaultDirPerm); err != nil {
		return err
	}

	// tell the files that we are creating it!
	sc.sendPath(path)

	// and run the files!
	return op(sc.ctx)
}

// CopyFile copies a file from src to dst.
func (sc *StagingContext) CopyFile(dst, src string) error {
	if err, ok := sc.ctxdone(); ok {
		return err
	}

	dstPath, err := sc.resolve(dst)
	if err != nil {
		return err
	}
	sc.sendPath(dst)
	return umaskfree.CopyFile(sc.ctx, dstPath, src)
}

// CopyDirectory copies a directory from src to dst.
func (sc *StagingContext) CopyDirectory(dst, src string) error {
	if err, ok := sc.ctxdone(); ok {
		return err
	}

	dstPath, err := sc.resolve(dst)
	if err != nil {
		return err
	}

	return umaskfree.CopyDirectory(sc.ctx, dstPath, src, func(dst, src string) {
		sc.sendPath(dst)
	})
}

// AddFile creates a new file at the provided path inside the destination.
// Passing the empty path creates the destination as a file.
//
// It then allows op to write to the file.
//
// The op function must not retain file.
// The underlying file does not need to be closed.
// AddFile will not return before op has returned.
func (sc *StagingContext) AddFile(path string, op func(ctx context.Context, file io.Writer) error) error {
	// check if we're already done
	if err, ok := sc.ctxdone(); ok {
		return err
	}

	// resolve the path!
	dst, err := sc.resolve(path)
	if err != nil {
		return err
	}

	// create the file
	file, err := umaskfree.Create(dst, umaskfree.DefaultFilePerm)
	if err != nil {
		return err
	}
	defer file.Close()

	// tell them that we are creating it!
	sc.sendPath(path)

	// and do whatever they wanted to do
	return op(sc.ctx, file)
}

func (sc *StagingContext) ctxdone() (err error, done bool) {
	err = sc.ctx.Err()
	done = (err != nil)
	return
}
