// Package volsnap captures and manages per-workload snapshots of an app's
// host-bind data volumes. It is deliberately independent of internal/backup
// (which is SQLite-specific): a snapshot here is a tar.gz of the resolved
// volume directories, recorded in the volume_snapshots table.
//
// Phase 2a-i covers CAPTURE only (create/list/delete/download). The restore
// path — which overwrites live data and needs container quiesce + atomic swap
// — is intentionally a separate, later phase.
package volsnap

import (
	"encoding/json"
	"os"

	"github.com/alexei/tinyforge/internal/store"
	"github.com/alexei/tinyforge/internal/volume"
)

// supportedScopes are the host-bind volume scopes phase 2a-i can snapshot.
// Each resolves to a real host directory the running container binds. Excluded
// for now: instance (needs the deployed image tag to resolve a per-tag dir),
// named/project_named (Docker named volumes — need a docker-run-tar primitive),
// and ephemeral (tmpfs — no data to capture).
var supportedScopes = map[string]bool{
	string(store.VolumeScopeAbsolute): true,
	string(store.VolumeScopeStage):    true,
	string(store.VolumeScopeProject):  true,
}

// SnapshotVolume is one volume covered by a snapshot. It is persisted in the
// snapshot row's manifest (JSON) and written into the archive so a future
// restore can re-resolve the target even if volume settings drift. Index names
// the archive subdirectory holding that volume's files.
type SnapshotVolume struct {
	Index  int    `json:"index"`
	Target string `json:"target"`
	Scope  string `json:"scope"`
	Source string `json:"source"`
}

// VolumeRef is a resolved, on-disk host-bind volume eligible for snapshotting.
type VolumeRef struct {
	Target   string
	Scope    string
	Source   string
	HostPath string
}

// SkippedVolume is a declared volume that cannot be snapshotted, with the
// reason surfaced to the UI so users are never misled into thinking data is
// captured when it is not.
type SkippedVolume struct {
	Target string `json:"target"`
	Scope  string `json:"scope"`
	Reason string `json:"reason"`
}

// scVolumes is the minimal shape parsed out of an image workload's
// source_config — just enough to learn its declared volumes without importing
// the image source package.
type scVolumes struct {
	Volumes []struct {
		Source string `json:"source"`
		Target string `json:"target"`
		Scope  string `json:"scope"`
		Name   string `json:"name"`
	} `json:"volumes"`
}

// SnapshotableVolumes enumerates a workload's data volumes and splits them into
// those that can be snapshotted now (resolved host-bind dirs that exist on
// disk) and those that are skipped (with a reason). It mirrors the image
// source's computeMounts merge: source_config volumes overlaid by persisted
// workload_volumes rows (persisted wins on a target conflict).
//
// Only image-source workloads declare host-bind data volumes today; for any
// other source kind both slices come back empty.
func SnapshotableVolumes(st *store.Store, w store.Workload, settings store.Settings) (refs []VolumeRef, skipped []SkippedVolume, err error) {
	if w.SourceKind != "image" {
		return nil, nil, nil
	}

	byTarget := map[string]store.WorkloadVolume{}

	var cfg scVolumes
	if w.SourceConfig != "" {
		// Best-effort: a malformed config simply yields no inline volumes; the
		// persisted rows below still apply.
		_ = json.Unmarshal([]byte(w.SourceConfig), &cfg)
	}
	for _, v := range cfg.Volumes {
		if v.Target == "" {
			continue
		}
		byTarget[v.Target] = store.WorkloadVolume{Source: v.Source, Target: v.Target, Scope: v.Scope, Name: v.Name}
	}
	persisted, perr := st.ListWorkloadVolumes(w.ID)
	if perr != nil {
		return nil, nil, perr
	}
	for _, p := range persisted {
		byTarget[p.Target] = store.WorkloadVolume{Source: p.Source, Target: p.Target, Scope: p.Scope, Name: p.Name}
	}

	params := volume.ResolveWorkloadParams{
		BasePath:           settings.BaseVolumePath,
		WorkloadID:         w.ID,
		WorkloadName:       w.Name,
		AllowedVolumePaths: settings.AllowedVolumePaths,
	}

	for _, v := range byTarget {
		if v.Target == "" {
			continue
		}
		if !supportedScopes[v.Scope] {
			skipped = append(skipped, SkippedVolume{Target: v.Target, Scope: v.Scope, Reason: skipReason(v.Scope)})
			continue
		}
		hostPath, rerr := volume.ResolveWorkloadPath(v, params)
		if rerr != nil {
			skipped = append(skipped, SkippedVolume{Target: v.Target, Scope: v.Scope, Reason: rerr.Error()})
			continue
		}
		info, serr := os.Stat(hostPath)
		if serr != nil || !info.IsDir() {
			skipped = append(skipped, SkippedVolume{Target: v.Target, Scope: v.Scope, Reason: "no data on disk yet"})
			continue
		}
		refs = append(refs, VolumeRef{Target: v.Target, Scope: v.Scope, Source: v.Source, HostPath: hostPath})
	}
	return refs, skipped, nil
}

func skipReason(scope string) string {
	switch scope {
	case string(store.VolumeScopeInstance):
		return "instance-scoped volumes are not yet snapshottable"
	case string(store.VolumeScopeNamed), string(store.VolumeScopeProjectNamed):
		return "Docker named volumes are not yet snapshottable"
	case string(store.VolumeScopeEphemeral):
		return "ephemeral (tmpfs) volumes hold no persistent data"
	default:
		return "unsupported volume scope"
	}
}
