package exporter

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

	"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
	"github.com/FAU-CDI/wisski-distillery/internal/models"
	"github.com/FAU-CDI/wisski-distillery/internal/wisski"
	"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/locker"
	"github.com/FAU-CDI/wisski-distillery/pkg/logging"
	"github.com/rs/zerolog"
	"github.com/tkw1536/pkglib/collection"
	"github.com/tkw1536/pkglib/contextx"
	"github.com/tkw1536/pkglib/status"
	"golang.org/x/exp/maps"
	"golang.org/x/exp/slices"
)

// SnapshotDescription is a description for a snapshot
type SnapshotDescription struct {
	Dest      string // destination path
	Keepalive bool   // should we keep the instance alive while making the snapshot?

	Parts []string // SnapshotName()s of the components to include.
}

// Snapshot represents the result of generating a snapshot
type Snapshot struct {
	Description SnapshotDescription
	Instance    models.Instance

	// Start and End Time of the snapshot
	StartTime time.Time
	EndTime   time.Time

	// Generic Panic that may have occured
	ErrPanic interface{}
	ErrStart error
	ErrStop  error

	// Errors holds errors for each component
	Errors map[string]error

	// Logs contains logfiles for each component
	Logs map[string]string

	// List of files included
	WithManifest

	// snapshotables that are running and not running
	partsRunning []component.Snapshotable `json:"-"`
	partsStopped []component.Snapshotable `json:"-"`
}

// Snapshot creates a new snapshot of this instance into dest
func (exporter *Exporter) NewSnapshot(ctx context.Context, instance *wisski.WissKI, progress io.Writer, desc SnapshotDescription) (snapshot Snapshot) {

	logging.LogMessage(progress, "Locking instance")
	if !instance.Locker().TryLock(ctx) {
		err := locker.Locked
		fmt.Fprintln(progress, err)
		fmt.Fprintln(progress, "Aborting snapshot creation")

		return Snapshot{
			ErrPanic: err,
		}
	}
	defer func() {
		logging.LogMessage(progress, "Unlocking instance")

		ctx, cancel := contextx.Anyways(ctx, time.Second)
		defer cancel()

		instance.Locker().Unlock(ctx)
	}()

	// setup the snapshot
	snapshot.Description = desc
	exporter.resolveParts(ctx, desc.Parts, &snapshot)
	snapshot.Instance = instance.Instance

	// capture anything critical, and write the end time
	defer func() {
		snapshot.ErrPanic = recover()
	}()

	// do the create keeping track of time!
	logging.LogOperation(func() error {
		snapshot.StartTime = time.Now().UTC()

		wboxerr, wboxmsg := snapshot.makeParts(ctx, progress, exporter, instance, false)
		bboxerr, bboxlog := snapshot.makeParts(ctx, progress, exporter, instance, true)

		snapshot.EndTime = time.Now().UTC()

		// collection all the errors and logs
		snapshot.Errors = collection.Append(wboxerr, bboxerr)
		snapshot.Logs = collection.Append(wboxmsg, bboxlog)

		return nil
	}, progress, "Writing snapshot files")

	slices.Sort(snapshot.Manifest)
	return
}

// resolveParts resolves parts, and writes it into snapshot.Description.Parts.
// Also sets up snapshot.partsRunning and snapshot.partsStopped.
// sends a warning about unknown parts into the logger in context.
func (snapshots *Exporter) resolveParts(ctx context.Context, parts []string, snapshot *Snapshot) {
	partMap := make(map[string]component.Snapshotable, len(snapshots.dependencies.Snapshotable))
	for _, part := range snapshots.dependencies.Snapshotable {
		partMap[part.SnapshotName()] = part
	}

	// filter the parts (if requested)
	if len(parts) != 0 {
		keys := make(map[string]struct{}, len(parts))
		for _, part := range parts {
			keys[part] = struct{}{}
		}

		// delete all the parts which weren't explicitly requested
		for part := range partMap {
			if _, ok := keys[part]; !ok {
				delete(partMap, part)
			} else {
				delete(keys, part)
			}
		}

		// throw a warning for unknown parts
		for key := range keys {
			zerolog.Ctx(ctx).Warn().Str("part", key).Msg("ignoring unknown snapshot part")
		}
	}

	// sort the names of all requested parts
	snapshot.Description.Parts = maps.Keys(partMap)
	slices.Sort(snapshot.Description.Parts)

	// and setup the map for running and stopped parts!
	for _, name := range snapshot.Description.Parts {
		part := partMap[name]
		if part.SnapshotNeedsRunning() {
			snapshot.partsRunning = append(snapshot.partsRunning, part)
		} else {
			snapshot.partsStopped = append(snapshot.partsStopped, part)
		}
	}
}

func (snapshot *Snapshot) makeParts(ctx context.Context, progress io.Writer, _ *Exporter, instance *wisski.WissKI, needsRunning bool) (errmap map[string]error, logmap map[string]string) {
	if !needsRunning && !snapshot.Description.Keepalive {
		stack := instance.Barrel().Stack()

		logging.LogMessage(progress, "Stopping instance")
		snapshot.ErrStop = stack.Down(ctx, progress)

		defer func() {
			logging.LogMessage(progress, "Starting instance")
			snapshot.ErrStart = stack.Up(ctx, progress)
		}()
	}
	// handle writing the manifest!
	manifest, done := snapshot.handleManifest(snapshot.Description.Dest)
	defer done()

	// create a new status
	st := status.NewWithCompat(progress, 0)
	st.Start()
	defer st.Stop()

	// get the components
	var comps []component.Snapshotable
	if needsRunning {
		comps = snapshot.partsRunning
	} else {
		comps = snapshot.partsStopped
	}

	// run each of the parts
	errors, ids := status.Group[component.Snapshotable, error]{
		PrefixString: func(item component.Snapshotable, index int) string {
			return fmt.Sprintf("[snapshot %q]: ", item.Name())
		},
		PrefixAlign: true,

		Handler: func(sc component.Snapshotable, index int, writer io.Writer) error {
			return sc.Snapshot(
				instance.Instance,
				component.NewStagingContext(
					ctx,
					writer,
					filepath.Join(snapshot.Description.Dest, sc.SnapshotName()),
					manifest,
				),
			)
		},

		ResultString: status.DefaultErrorString[component.Snapshotable],
	}.Use(st, comps)

	// keep all the log files
	files := st.Keep()

	// store errors and logs
	errmap = make(map[string]error, len(comps))
	logmap = make(map[string]string, len(comps))

	for i, wc := range comps {
		name := wc.SnapshotName()
		errmap[name] = errors[i]

		// read the logfile
		logfile := files[ids[i]]
		bytes, err := os.ReadFile(logfile)
		if err != nil {
			zerolog.Ctx(ctx).Err(err).Str("component", name).Msg("unable to copy logfile")
			continue
		}

		// delete it, but store the content in the results
		os.Remove(logfile)
		logmap[name] = string(bytes)
	}

	return
}
