package backup

import (
	"context"
	"fmt"
	"strings"
	"time"

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

// Offsite shares leg — R-7b Part 3, the remote leg of Model B′.
//
// It is a SIBLING of the per-app loop in runOffboxInternal, not a modification of it. The B′
// invariant — every per-app restic invocation stays byte-identical — is the headline guarantee here
// and is enforced by TestOffboxSharesLegLeavesAppCallsByteIdentical.
//
// Shape: ONE additional `restic backup` call tagged [felhom-offbox, _shares], whose paths are the
// payload staging dir plus every MANDATORY (Felhőmentés-on) share folder. It reuses resticStep (so
// it inherits the C2 crash-lock self-heal), the caller's already-ensured repo, the caller's
// already-taken single-flight, and the SAME enlargement-gate arithmetic the per-app path uses. It
// runs BEFORE retention, so `forget --group-by host,tags` covers the `_shares` group for free with
// no flag change.
//
// Degradation contract: when the quota gate trips, the push degrades to the MANIFEST ONLY — never to
// nothing. Definitions protection must not regress just because the files no longer fit; a customer
// who is over quota should still get their „Megosztás" page back from a DR restore.

// sharesLegResult carries the outcome of the offsite shares leg back to the run.
type sharesLegResult struct {
	ran      bool  // the leg produced a restic call
	count    int   // share folders included (0 = definitions-only push)
	blocked  bool  // the quota gate degraded this push to manifest-only
	estBytes int64 // the estimate the gate weighed (for the blocked notification)
	warns    []string
	status   string // persisted SharesLastStatus
}

// runOffboxSharesLeg pushes the shares source. Caller holds the running flag and has already ensured
// the repo. Returns the leg result plus a hard error only when the restic call itself failed.
func (m *Manager) runOffboxSharesLeg(ctx context.Context, base, env []string, t *settings.OffboxTarget) (sharesLegResult, error) {
	var res sharesLegResult
	if !m.sharesEnabled() {
		// Sharing off / no shares registered: a clean no-op. NO `_shares` restic group is created —
		// an empty group would age through retention forever and imply a protection that isn't there.
		return res, nil
	}

	shares := m.classifiedShares()
	var mandatory []classifiedShare
	for _, sh := range shares {
		if sh.mandatory {
			mandatory = append(mandatory, sh)
		}
	}
	if len(shares) > 0 && len(mandatory) == 0 {
		// Every share is tier-2-only. The FILES correctly stay off-site-excluded (Scenario B), but the
		// definitions still ride offsite: they are ~1 KB and they are what makes a DR restore give the
		// customer their share configuration back rather than an empty page.
		m.logger.Printf("[INFO] [shares] offsite: no share is marked for the cloud — pushing share definitions only")
	}

	payloadDir, passdbOK, perr := m.buildSharesPayload()
	if perr != nil {
		// Without a payload there is nothing to anchor a restore on; push the files anyway rather than
		// skipping protection, but say so loudly.
		m.logger.Printf("[ERROR] [shares] offsite: payload staging failed — pushing share files without the definition manifest: %v", perr)
		res.warns = append(res.warns, "A megosztás-beállítások távoli mentése nem sikerült — a fájlok mentése megtörtént.")
		payloadDir = ""
	}
	if !passdbOK {
		res.warns = append(res.warns, "A megosztás jelszava nem került a mentésbe (a megosztás szolgáltatás nem futott) — visszaállítás után újra meg kell adni.")
	}

	paths := make([]string, 0, len(mandatory)+1)
	if payloadDir != "" {
		paths = append(paths, payloadDir)
	}
	sharePaths := make([]string, 0, len(mandatory))
	for _, sh := range mandatory {
		sharePaths = append(sharePaths, sh.Path)
	}

	// Pre-push enlargement gate — the SAME arithmetic as the per-app path (offbox.go): last-known repo
	// raw-data bytes + this push's estimate crossing the soft quota degrades the push instead of
	// failing it. Here the degradation floor is the manifest rather than a recovery unit.
	if len(sharePaths) > 0 && t != nil && t.QuotaGB > 0 {
		var est int64
		for _, p := range sharePaths {
			est += m.offboxSize()(p)
		}
		if t.RepoSizeBytes+est >= int64(t.QuotaGB)*offboxGiB {
			m.logger.Printf("[INFO] [shares] offsite: enlargement blocked by quota (est %s + repo %s ≥ %d GB) — definitions-only push continues",
				humanizeBytes(est), humanizeBytes(t.RepoSizeBytes), t.QuotaGB)
			res.blocked = true
			res.estBytes = est
			sharePaths = nil
		}
	}
	paths = append(paths, sharePaths...)
	if len(paths) == 0 {
		m.logger.Printf("[WARN] [shares] offsite: nothing to push (no payload, no eligible share) — skipped")
		res.status = "skipped"
		return res, nil
	}

	args := append([]string{"backup", "--tag", "felhom-offbox", "--tag", SharesPseudoStack}, paths...)
	bctx, cancel := context.WithTimeout(ctx, offboxBackupTimeout)
	out, berr := m.resticStep(bctx, env, base, "backup:"+SharesPseudoStack, args...)
	cancel()
	if berr != nil {
		m.logger.Printf("[ERROR] [shares] offsite push failed: %v: %s", berr, truncate(out))
		res.status = "error"
		return res, fmt.Errorf("offbox backup %s: %w", SharesDisplayName, berr)
	}
	res.ran = true
	res.count = len(sharePaths)
	res.status = "ok"
	if res.blocked {
		res.status = "blocked"
	}
	m.logger.Printf("[INFO] [shares] offsite push OK: %d share folder(s) + definitions", res.count)
	return res, nil
}

// recordSharesOffsiteStatus persists the per-tier status the „Megosztás" page renders. Kept separate
// from the app-wide offsite status so a page can state SHARES truth without inferring it.
func (m *Manager) recordSharesOffsiteStatus(res sharesLegResult) {
	if m.settings == nil || res.status == "" {
		return
	}
	if err := m.settings.UpdateOffboxStatus(func(o *settings.OffboxTarget) {
		o.SharesLastRun = time.Now().UTC().Format(time.RFC3339)
		o.SharesLastStatus = res.status
		o.SharesLastCount = res.count
	}); err != nil {
		m.logger.Printf("[WARN] [shares] offsite status persist failed: %v", err)
	}
}

// SharesOffsiteStatus returns the last shares-leg outcome for the „Megosztás" page: the RFC3339 run
// stamp, the status label and how many share folders the push covered. ok=false when no offsite
// target is configured or the leg has never run.
func (m *Manager) SharesOffsiteStatus() (lastRun, status string, count int, ok bool) {
	if m.settings == nil {
		return "", "", 0, false
	}
	t := m.settings.GetOffboxTarget()
	if t == nil || t.SharesLastStatus == "" {
		return "", "", 0, false
	}
	return t.SharesLastRun, t.SharesLastStatus, t.SharesLastCount, true
}

// sharesBlockedWarning renders the customer-facing note for a quota-degraded shares push. It goes
// through DisplayStackName's vocabulary deliberately: the reserved `_shares` key must never appear
// in Hungarian prose.
func sharesBlockedWarning() string {
	return fmt.Sprintf("Figyelmeztetés: a tárhelykeret miatt a(z) %s tartalma nem került a távoli mentésbe — csak a megosztás-beállítások.",
		strings.ToLower(SharesDisplayName))
}
