package backup

import (
	"os"
	"path/filepath"
	"strings"
	"time"
)

// RestorePoint describes one restorable keep-side backup for the /backups restore panel
// (GET /api/backup/snapshots). The field names/shape are the payload contract of the
// backups.html restore JS (formatSnapshot): time / short_id / tier / drive_label.
//
// The keep-side restore has exactly ONE restore point per app — the current recovery unit
// (RestoreFromRecoveryUnit reads "the unit", not a history; snapshot_id is logging-only).
// Tier is always 1: Tier-2 copies are NOT restorable through POST /backup/restore (it only
// reads the app's primary unit), so listing them would silently restore tier-1 data while
// claiming tier-2 — never emit them here.
type RestorePoint struct {
	Time       string `json:"time"`        // RFC3339 — newest artifact in the unit
	ShortID    string `json:"short_id"`    // opaque label; POST /backup/restore uses it for logging only
	Tier       int    `json:"tier"`        // always 1 (see above)
	DriveLabel string `json:"drive_label"` // registered storage label; empty for the SSD fallback
}

// restorePointShortID is the single keep-side restore point's identifier. Hungarian ("local"),
// because the JS renders it verbatim inside the snapshot dropdown label.
const restorePointShortID = "helyi"

// ListRestorePoints returns the app's restorable keep-side backups, and whether the stack is
// known at all (found=false → the caller should 404). A known stack with no recovery unit on
// disk returns an EMPTY list (a valid answer — "no backup yet"), not an error.
//
// The single point's Time is the newest mtime among the unit's artifacts (manifest.json,
// db-dumps/*.sql, volume-dumps/*.tar): the manifest is only rewritten when the app's config
// changes (checksum-skip), so the nightly-refreshed dumps are usually the freshest artifact.
func (m *Manager) ListRestorePoints(stackName string) (points []RestorePoint, found bool) {
	if m.stackProvider == nil {
		return nil, false
	}
	if _, ok := m.stackProvider.GetStackComposePath(stackName); !ok {
		return nil, false
	}

	nsRoot := m.AppNamespaceRoot(stackName)
	if nsRoot == "" || !filepath.IsAbs(nsRoot) {
		// Stack is known but its backup location is unresolvable (e.g. systemDataPath unset in a
		// misconfigured environment) — honest empty list rather than a path walk from "".
		m.logger.Printf("[WARN] [backup] ListRestorePoints(%s): cannot resolve namespace root", stackName)
		return []RestorePoint{}, true
	}

	fi, err := os.Stat(RecoveryUnitManifestPath(nsRoot, stackName))
	if err != nil {
		return []RestorePoint{}, true // no recovery unit yet — "no backup" is a valid answer
	}
	newest := fi.ModTime()
	newest = newestArtifact(AppDBDumpPath(nsRoot, stackName), ".sql", newest)
	newest = newestArtifact(AppVolumeDumpPath(nsRoot, stackName), ".tar", newest)

	driveLabel := ""
	if drive := m.GetAppDrivePath(stackName); drive != "" && drive != m.systemDataPath && m.settings != nil {
		driveLabel = m.settings.GetStorageLabel(drive)
	}

	return []RestorePoint{{
		Time:       newest.UTC().Format(time.RFC3339),
		ShortID:    restorePointShortID,
		Tier:       1,
		DriveLabel: driveLabel,
	}}, true
}

// newestArtifact returns the newest mtime among cur and the files with the given extension in
// dir (non-recursive; a missing dir contributes nothing).
func newestArtifact(dir, ext string, cur time.Time) time.Time {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return cur
	}
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ext) {
			continue
		}
		if info, err := e.Info(); err == nil && info.ModTime().After(cur) {
			cur = info.ModTime()
		}
	}
	return cur
}
