package backup

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

// Verification copies — the listing/delete surface for `<nsRoot>/backups/offsite-restore/<app>`.
//
// WHY THIS EXISTS (v0.147.0, feedback slice 4a): an offsite verification restore wrote its result to
// a path the customer was never told, and nothing anywhere listed what had accumulated. Pressing
// „Ellenőrző visszaállítás" produced a flash saying it had been restored "to a verification folder
// on the drive" — which folder, on which drive, and how much space it was now using were all
// invisible. So copies piled up and the only way to find them was SSH.
//
// The path segments were already open-coded in three places; offsiteRestoreRootFor() is now the one
// place `backups/offsite-restore` is spelled, and offboxRestoreScratchDir() builds on it.

// OffsiteRestoreCopy is one verification copy on disk.
type OffsiteRestoreCopy struct {
	Stack     string    `json:"stack"`      // app slug, or SharesPseudoStack for the shares copy
	Path      string    `json:"path"`       // absolute path — the thing the customer could not see
	Size      int64     `json:"size"`       // bytes
	SizeHuman string    `json:"size_human"` // pre-humanized for the template
	Created   time.Time `json:"created"`    // dir mtime; restic writes the tree once, so this is the restore time
}

// offsiteRestoreRootFor returns `<nsRoot>/backups/offsite-restore` for a drive path. THE single place
// these segments are written.
func (m *Manager) offsiteRestoreRootFor(drivePath string) string {
	return filepath.Join(m.namespaceRoot(drivePath), "backups", "offsite-restore")
}

// offsiteRestoreDriveRoots returns every drive path a verification copy could live under, in the same
// preference order offboxRestoreScratchDir uses to CHOOSE one — so listing can never miss a copy the
// restore path was capable of creating. Deduplicated, order preserved.
func (m *Manager) offsiteRestoreDriveRoots() []string {
	seen := map[string]bool{}
	var roots []string
	add := func(p string) {
		p = strings.TrimSpace(p)
		if p == "" || seen[p] {
			return
		}
		seen[p] = true
		roots = append(roots, p)
	}
	// App HDDs first (offboxRestoreScratchDir's rule 1), then every schedulable path (rules 2 and 3).
	if m.stackProvider != nil {
		for _, s := range m.stackProvider.ListDeployedStacks() {
			add(m.stackProvider.GetStackHDDPath(s.Name))
		}
	}
	if m.settings != nil {
		for _, sp := range m.settings.GetSchedulableStoragePaths() {
			add(sp.Path)
		}
	}
	return roots
}

// ListOffsiteRestoreCopies enumerates every verification copy across every candidate drive, newest
// first. Missing directories are not an error — "none yet" is the normal state.
func (m *Manager) ListOffsiteRestoreCopies() []OffsiteRestoreCopy {
	sizer := m.offboxSize()
	var out []OffsiteRestoreCopy
	seen := map[string]bool{}
	for _, drive := range m.offsiteRestoreDriveRoots() {
		root := m.offsiteRestoreRootFor(drive)
		entries, err := os.ReadDir(root)
		if err != nil {
			continue // no copies on this drive (or the drive is not mounted) — not an error
		}
		for _, e := range entries {
			if !e.IsDir() {
				continue
			}
			p := filepath.Join(root, e.Name())
			if seen[p] {
				continue // two stacks can resolve to the same drive; list each path once
			}
			seen[p] = true
			c := OffsiteRestoreCopy{Stack: e.Name(), Path: p}
			if fi, err := e.Info(); err == nil {
				c.Created = fi.ModTime()
			}
			c.Size = sizer(p)
			c.SizeHuman = humanizeBytes(c.Size)
			out = append(out, c)
		}
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Created.After(out[j].Created) })
	return out
}

// DeleteOffsiteRestoreCopy removes ONE verification copy.
//
// This is the only delete path v0.147.0 adds, so it is guarded twice over. The stack name must pass
// isSafeStackName (no separators, no traversal), and the resolved path must sit STRICTLY INSIDE a
// `backups/offsite-restore` root that this Manager itself computed — a path that merely looks right
// is refused. Both checks are on the RESOLVED path, not the input, so a symlinked scratch cannot
// walk the delete out of the sandbox.
func (m *Manager) DeleteOffsiteRestoreCopy(stack string) error {
	if !isSafeStackName(stack) {
		return fmt.Errorf("érvénytelen alkalmazásnév")
	}
	for _, drive := range m.offsiteRestoreDriveRoots() {
		root := m.offsiteRestoreRootFor(drive)
		target := filepath.Join(root, stack)

		fi, err := os.Stat(target)
		if err != nil || !fi.IsDir() {
			continue
		}
		// Prefix safety: only ever remove strictly inside `backups/offsite-restore/`. Same shape as
		// the F5 stale-primary prune (backup.go) — refuse loudly rather than best-effort skip, since
		// reaching here with an out-of-sandbox path means a helper above is wrong.
		cleanTarget := filepath.Clean(target)
		cleanRoot := filepath.Clean(root) + string(filepath.Separator)
		if !strings.HasPrefix(cleanTarget+string(filepath.Separator), cleanRoot) {
			m.logger.Printf("[WARN] [offbox] refusing to delete verification copy outside %s: %s", root, cleanTarget)
			return fmt.Errorf("a törlés útvonala kívül esik az ellenőrző mappán")
		}
		if err := os.RemoveAll(cleanTarget); err != nil {
			return fmt.Errorf("a másolat törlése nem sikerült: %w", err)
		}
		m.logger.Printf("[INFO] [offbox] deleted verification copy: %s", cleanTarget)
		return nil
	}
	return fmt.Errorf("nincs ilyen ellenőrző másolat")
}

// OffsiteRestoreScratchPath exposes WHERE a verification restore for stack would land, so the UI can
// name the full path in the completion message instead of saying "a verification folder somewhere".
func (m *Manager) OffsiteRestoreScratchPath(stack string) string {
	scratch, _, err := m.offboxRestoreScratchDir(stack)
	if err != nil {
		return ""
	}
	return scratch
}
