package backup

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

	"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)

// Tier-2 shares job — R-7b Part 2, the local cross-drive leg of Model B′.
//
// It runs AFTER the per-stack tier-2 loop, inside the SAME orchestrator run, and it is a SIBLING of
// that loop, never a modification of it: RunTier2 and its per-app dest tree are untouched (the B′
// invariant, enforced by TestSharesTier2LeavesPerAppTreeUntouched). What it reuses instead is every
// primitive the per-app path proved: the tier2Mirror seam, selectTier2TargetFrom's target choice and
// headroom math, tier2ReconcileRoots' staleness pruning, tier2SafeRemove's destBase-bounded removal,
// the marker-written-LAST discipline, and the recordTier2* status helpers.
//
// Layout — backups/secondary/_shares/ on the target drive:
//
//	.felhom-tier2-layout        marker, content "2", written LAST (after every leg + reconcile)
//	_payload/                   the share definitions + credential copy (shares_payload.go)
//	<sourceDriveKey>/<share>/   one mirrored leg per share, grouped by the drive it came from
//
// Shares are grouped BY SOURCE DRIVE because a household's shares can span several drives and each
// group needs its own cross-drive target: a leg's target may never be that leg's own source drive.

// sharesPayloadDestRel is the payload's relpath inside the shares dest (a reserved root name; the
// leading underscore cannot collide with a drive key because drive keys are derived from paths).
const sharesPayloadDestRel = "_payload"

// sharesDriveKey turns an absolute source-drive path into ONE safe dest path segment. The full path
// is encoded (not just its basename) so two drives whose mountpoints share a basename — /mnt/a/data
// and /mnt/b/data — can never map onto the same dest subtree and silently overwrite each other.
// Deterministic and stable across runs, which is what lets the reconcile pass recognise its own dirs.
func sharesDriveKey(drive string) string {
	s := strings.Trim(filepath.ToSlash(drive), "/")
	if s == "" {
		return "root"
	}
	var b strings.Builder
	for _, r := range s {
		switch {
		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '-':
			b.WriteRune(r)
		default:
			b.WriteByte('_')
		}
	}
	return b.String()
}

// shareSourceDrive resolves the registered storage root a share's folder lives under. Returns "" when
// the share sits under no registered root — such a share has no meaningful "other drive" and is
// skipped with a warning rather than guessed at.
func (m *Manager) shareSourceDrive(path string) string {
	if m.settings == nil {
		return ""
	}
	p := filepath.ToSlash(path)
	best := ""
	for _, sp := range m.settings.GetStoragePaths() {
		root := filepath.ToSlash(sp.Path)
		if root == "" {
			continue
		}
		// Longest match wins, so a nested registered root claims its own shares. Containment is
		// compared in slash form, but the returned value is the REGISTRY'S OWN string: the target
		// selector compares the source drive against sp.Path by equality, and handing it a normalised
		// variant would make that comparison miss and let a group target its own source drive.
		if (p == root || strings.HasPrefix(p, root+"/")) && len(root) > len(best) {
			best = sp.Path
		}
	}
	return best
}

// sharesTier2Group is one source drive's worth of shares plus the target chosen for it.
type sharesTier2Group struct {
	sourceDrive string
	key         string
	shares      []classifiedShare
}

// RunSharesTier2 mirrors every classified, available share to a cross-drive target and stages the
// payload beside it. Best-effort and idempotent, exactly like RunTier2: an absent target is an
// honest recorded status, not an error. Returns the first hard copy error.
func (m *Manager) RunSharesTier2() error {
	if !m.sharesEnabled() {
		// Feature off or no shares registered: a clean no-op. Deliberately NOT a recorded status —
		// writing one would make the „Megosztás" page claim a backup tier for a feature in use by
		// nobody, and would create the dest tree for zero shares.
		return nil
	}
	if m.settings != nil {
		if cd := m.settings.GetCrossDriveConfig(SharesPseudoStack); cd != nil && cd.UserDisabled {
			m.logger.Printf("[INFO] [shares] tier-2 skipped — disabled by customer")
			return nil
		}
	}
	shares := m.classifiedShares()
	if len(shares) == 0 {
		// Every registered share is on a drive that is away / a folder that vanished. That is a real
		// operational state the customer must see, not silence.
		m.recordTier2NoTarget(SharesPseudoStack, "egyetlen megosztás mappája sem érhető el — ellenőrizd a meghajtókat")
		m.logger.Printf("[WARN] [shares] tier-2: no available share folders — nothing mirrored")
		return nil
	}

	// Group by source drive (deterministic order so logs and dest trees are reproducible).
	byDrive := map[string]*sharesTier2Group{}
	var warns []string
	for _, sh := range shares {
		drive := m.shareSourceDrive(sh.Path)
		if drive == "" {
			m.logger.Printf("[WARN] [shares] tier-2: share %s is not under a registered storage root — skipped", sh.Name)
			warns = append(warns, fmt.Sprintf("A(z) „%s” megosztás nem regisztrált adatmeghajtón van — kimaradt a 2. mentésből.", sh.Name))
			continue
		}
		g := byDrive[drive]
		if g == nil {
			g = &sharesTier2Group{sourceDrive: drive, key: sharesDriveKey(drive)}
			byDrive[drive] = g
		}
		g.shares = append(g.shares, sh)
	}
	var groups []*sharesTier2Group
	for _, g := range byDrive {
		groups = append(groups, g)
	}
	sort.Slice(groups, func(i, j int) bool { return groups[i].sourceDrive < groups[j].sourceDrive })
	if len(groups) == 0 {
		m.recordTier2NoTarget(SharesPseudoStack, "a megosztások egyike sincs regisztrált adatmeghajtón")
		return nil
	}

	// Payload once per run — both tiers read the same staged directory.
	payloadDir, _, perr := m.buildSharesPayload()
	if perr != nil {
		m.logger.Printf("[WARN] [shares] tier-2: payload staging failed — mirroring files without the definition manifest: %v", perr)
		warns = append(warns, "A megosztás-beállítások mentése nem sikerült — a fájlok mentése megtörtént.")
		payloadDir = ""
	}

	mirror := m.tier2Mirror
	if mirror == nil {
		mirror = rsyncMirror
	}
	start := time.Now()
	var (
		totalSize   int64
		lastTarget  *Tier2Target
		noTargetWhy []string
		mirrored    int
	)
	for _, g := range groups {
		// Two sizes, mirroring RunTier2's contract: full = every share in the group; state-only = the
		// MANDATORY (Felhőmentés-on) subset, which is what the SSD headroom guard must fit.
		var fullSize, stateOnlySize int64
		for _, sh := range g.shares {
			sz := dirSizeBytes(sh.Path)
			fullSize += sz
			if sh.mandatory {
				stateOnlySize += sz
			}
		}
		target, err := m.selectTier2TargetFrom(SharesPseudoStack, g.sourceDrive, fullSize, stateOnlySize)
		if err != nil {
			why := tier2NoTargetReason(err)
			noTargetWhy = append(noTargetWhy, fmt.Sprintf("%s: %s", g.sourceDrive, why))
			m.logger.Printf("[INFO] [shares] tier-2: no off-drive target for shares on %s — %s", g.sourceDrive, why)
			continue
		}
		// Defense-in-depth off-drive guard (selection already enforced it): a leg's target may never
		// be that leg's own source drive — that would be a same-disk "copy" pretending to be tier 2.
		if m.sameDevice(g.sourceDrive, target.NamespaceRoot) {
			noTargetWhy = append(noTargetWhy, fmt.Sprintf("%s: a kiválasztott cél ugyanazon a fizikai lemezen van", g.sourceDrive))
			continue
		}
		legs := g.shares
		if target.StateOnly {
			kept := legs[:0]
			dropped := false
			for _, sh := range legs {
				if sh.mandatory {
					kept = append(kept, sh)
				} else {
					dropped = true
				}
			}
			legs = kept
			if dropped {
				warns = append(warns, "A belső SSD-re csak a felhőmentésre jelölt megosztások férnek el — a többi nem került másolásra.")
			}
		}

		destBase := filepath.Join(target.NamespaceRoot, "backups", "secondary", SharesPseudoStack)
		legRels := make([]string, 0, len(legs)+1)
		for _, sh := range legs {
			rel := g.key + "/" + sh.Name
			if err := mirror(sh.Path, filepath.Join(destBase, filepath.FromSlash(rel))); err != nil {
				m.recordTier2Failure(SharesPseudoStack, target, err)
				if m.tier2Notify != nil {
					// DISPLAY BOUNDARY: the notifier feeds Hungarian customer/operator copy, so the
					// reserved key is mapped HERE rather than at each wiring site — a live demo run
					// pushed a literal "_shares" event before this was moved to the source.
					m.tier2Notify(DisplayStackName(SharesPseudoStack), target.Label, time.Since(start), err)
				}
				return fmt.Errorf("tier2 shares mirror %s: %w", sh.Name, err)
			}
			legRels = append(legRels, rel)
			totalSize += dirSizeBytes(sh.Path)
			mirrored++
		}
		// The payload rides every target so each one is independently restorable.
		if payloadDir != "" {
			if err := mirror(payloadDir, filepath.Join(destBase, sharesPayloadDestRel)); err != nil {
				m.logger.Printf("[WARN] [shares] tier-2: payload mirror to %s failed (files are copied): %v", destBase, err)
			} else {
				legRels = append(legRels, sharesPayloadDestRel)
			}
		}
		// Prune dest dirs no share covers any more (a share deleted or renamed since the last run
		// stops occupying the secondary drive within one run), then the marker LAST — a half-written
		// dest therefore has no marker and the restore path refuses it until the next good run.
		m.tier2ReconcileRoots(destBase, sharesTier2DestRoots(destBase), legRels)
		if err := m.writeTier2Marker(destBase); err != nil {
			m.logger.Printf("[WARN] [shares] tier-2: layout marker write failed (restore will refuse until next run): %v", err)
		}
		lastTarget = target
		m.logger.Printf("[INFO] [shares] tier-2 copied %d share(s) from %s → %s (%s)",
			len(legs), g.sourceDrive, destBase, humanizeBytes(totalSize))
	}

	dur := time.Since(start)
	if lastTarget == nil {
		reason := strings.Join(noTargetWhy, "; ")
		if reason == "" {
			reason = "nincs másik fizikai meghajtó — a 2. mentéshez 2. meghajtó szükséges"
		}
		m.recordTier2NoTarget(SharesPseudoStack, reason)
		m.logger.Printf("[INFO] [shares] tier-2: no off-drive target for any share group — %s", reason)
		return nil
	}
	if len(noTargetWhy) > 0 {
		warns = append(warns, "Néhány meghajtón lévő megosztásnak nincs másodlagos célja: "+strings.Join(noTargetWhy, "; "))
	}
	m.recordTier2Success(SharesPseudoStack, lastTarget, totalSize, strings.Join(warns, " "), dur)
	if m.tier2Notify != nil {
		m.tier2Notify(DisplayStackName(SharesPseudoStack), lastTarget.Label, dur, nil)
	}
	m.logger.Printf("[INFO] [shares] tier-2 run complete: %d share leg(s), %s, %s",
		mirrored, humanizeBytes(totalSize), dur.Round(time.Second))
	return nil
}

// sharesTier2DestRoots lists the top-level dirs under a shares destBase that the reconcile pass may
// walk. It reads what is ON DISK rather than what this run produced, so a drive key from a drive that
// is no longer registered still gets visited (and pruned) instead of lingering forever.
func sharesTier2DestRoots(destBase string) []string {
	entries, err := os.ReadDir(destBase)
	if err != nil {
		return nil
	}
	var roots []string
	for _, e := range entries {
		if e.IsDir() {
			roots = append(roots, e.Name())
		}
	}
	sort.Strings(roots)
	return roots
}

// writeTier2Marker writes the shared layout marker (content "2") — the LAST write of a dest, so its
// presence means "every leg and the reconcile completed".
func (m *Manager) writeTier2Marker(destBase string) error {
	return os.WriteFile(filepath.Join(destBase, tier2LayoutMarker), []byte(tier2LayoutVersion), 0o644)
}

// SharesTier2Status returns the recorded shares tier-2 status for the „Megosztás" page (nil when the
// job has never run). It is the SAME CrossDriveBackup record the per-app rows use, keyed by the
// reserved pseudo-stack.
func (m *Manager) SharesTier2Status() *settings.CrossDriveBackup {
	if m.settings == nil {
		return nil
	}
	return m.settings.GetCrossDriveConfig(SharesPseudoStack)
}
