package web

// Offsite pool-box aggregate surfaces (v0.64.0, R-5): the Offsite-tab panel + the Dashboard tile.
// The web layer NEVER fetches from Hetzner — it reads the checker's cached snapshot (s.offsiteBox) and
// composes per-customer rows from state the hub already holds (quota from the ConfigJSON Descriptor,
// usage from the controller report echo).

import (
	"encoding/json"
	"fmt"
	"sort"
	"time"

	"gitea.dooplex.hu/admin/felhom-hub/internal/offsite"
)

// offsiteBoxView is the pool-box panel model. Every display value is composed handler-side; band
// strings ("ok"/"warning"/"critical") drive the bar color via templates.
type offsiteBoxView struct {
	Configured  bool // s.offsiteBox wired (HETZNER_TOKEN + box id present)
	Pending     bool // configured but the first refresh hasn't landed yet
	Degraded    bool // last fetch failed / zero capacity — values shown stale
	CapacityStr string
	UsedStr     string
	DataStr     string
	SnapshotStr string
	FillPercent float64
	FillBand    string
	SumQuotaStr string
	RatioStr    string // "1.17×"
	OversubBand string
	BoxType     string
	FetchedAt   time.Time
}

// offsiteCustomerRow is one per-customer usage/quota row under the panel.
type offsiteCustomerRow struct {
	CustomerName string
	Dedicated    bool
	QuotaStr     string  // "" for dedicated
	UsageStr     string  // "" when NoUsage
	UsageBytes   int64   // sort key
	UsagePercent float64 // usage/quota (shared only)
	UsageBand    string  // ok|warning|critical for the bar
	HasBar       bool    // shared + quota>0 + has usage → render a bar
	NoUsage      bool    // no offsite object in the report yet
}

// fmtBytesGB renders bytes as GB (one decimal) below 1 TB, TB (two decimals) above.
func fmtBytesGB(b int64) string {
	if b >= 1<<40 {
		return fmt.Sprintf("%.2f TB", float64(b)/float64(int64(1)<<40))
	}
	return fmt.Sprintf("%.1f GB", float64(b)/float64(int64(1)<<30))
}

// pctBand maps a percent to a band with the given warn/crit (web-side twin of monitor.bandForPercent —
// unexported there; a 3-line copy keeps web from depending on monitor internals).
func pctBand(pct, warn, crit float64) string {
	switch {
	case pct >= crit:
		return "critical"
	case pct >= warn:
		return "warning"
	default:
		return "ok"
	}
}

// offsiteUsageBytes reads repo_size_bytes from a customer's report `offsite` object — the ONLY place the
// live repo size exists. ok=false when the report carries no offsite object (never reported). Quota is
// NEVER taken from here (the report echo would undercount promises) — only usage. Twin of monitor's
// parseOffsite, scoped to the one field the rows need.
func offsiteUsageBytes(reportJSON string) (int64, bool) {
	var r struct {
		Offsite *struct {
			RepoSizeBytes int64 `json:"repo_size_bytes"`
		} `json:"offsite"`
	}
	if json.Unmarshal([]byte(reportJSON), &r) != nil || r.Offsite == nil {
		return 0, false
	}
	return r.Offsite.RepoSizeBytes, true
}

// offsiteTile is the compact Dashboard tile: fill% + ratio, band-colored, linking to /offsite.
type offsiteTile struct {
	FillPercent float64
	RatioStr    string
	Band        string // worst of fill/oversub → tile color
	Degraded    bool
}

// offsiteBoxTile builds the Dashboard tile, or nil when there is nothing to show (no provider, or the
// first refresh hasn't landed) — the caller then renders NO tile (never a zeroed one).
func (s *Server) offsiteBoxTile() *offsiteTile {
	if s.offsiteBox == nil {
		return nil
	}
	snap, ok := s.offsiteBox()
	if !ok {
		return nil // configured but no snapshot yet — absent, not a zeroed tile
	}
	band := "ok"
	switch {
	case snap.FillBand == "critical":
		band = "critical"
	case snap.FillBand == "warning" || snap.OversubBand == "warning":
		band = "warning"
	}
	return &offsiteTile{
		FillPercent: snap.FillPercent,
		RatioStr:    fmt.Sprintf("%.2f×", snap.Ratio),
		Band:        band,
		Degraded:    snap.Degraded,
	}
}

// offsiteBoxData builds the panel view + per-customer rows. Nil provider → Configured:false (the panel
// renders the honest "not configured"). Never fetches.
func (s *Server) offsiteBoxData() (offsiteBoxView, []offsiteCustomerRow) {
	if s.offsiteBox == nil {
		return offsiteBoxView{Configured: false}, nil
	}
	view := offsiteBoxView{Configured: true}
	if snap, ok := s.offsiteBox(); ok {
		view.Pending = false
		view.Degraded = snap.Degraded
		view.CapacityStr = fmtBytesGB(snap.CapacityBytes)
		view.UsedStr = fmtBytesGB(snap.UsedBytes)
		view.DataStr = fmtBytesGB(snap.DataBytes)
		view.SnapshotStr = fmtBytesGB(snap.SnapshotBytes)
		view.FillPercent = snap.FillPercent
		view.FillBand = snap.FillBand
		view.SumQuotaStr = fmt.Sprintf("%d GB", snap.SumQuotaGB)
		view.RatioStr = fmt.Sprintf("%.2f×", snap.Ratio)
		view.OversubBand = snap.OversubBand
		view.BoxType = snap.BoxType
		view.FetchedAt = snap.FetchedAt
	} else {
		view.Pending = true // configured, first fetch pending
	}
	return view, s.offsiteCustomerRows()
}

// offsiteCustomerRows composes the per-customer rows for every ENABLED-offsite customer: quota from the
// authoritative ConfigJSON Descriptor, usage from the report echo. Shared customers get a usage/quota
// bar; dedicated customers are listed without one. Sorted by usage descending.
func (s *Server) offsiteCustomerRows() []offsiteCustomerRow {
	cfgs, err := s.store.ListCustomerConfigs()
	if err != nil {
		s.logger.Printf("[WARN] offsite box: list configs: %v", err)
		return nil
	}
	usage := map[string]int64{}
	hasReport := map[string]bool{}
	if custs, cerr := s.store.GetCustomers(); cerr == nil {
		for _, c := range custs {
			if b, ok := offsiteUsageBytes(c.ReportJSON); ok {
				usage[c.CustomerID], hasReport[c.CustomerID] = b, true
			}
		}
	}
	var rows []offsiteCustomerRow
	for _, cfg := range cfgs {
		d, derr := offsite.ReadDescriptor(cfg.ConfigJSON)
		if derr != nil || d == nil || !d.Enabled {
			continue // only customers actually using an offsite tier appear
		}
		name := cfg.CustomerName
		if name == "" {
			name = cfg.CustomerID
		}
		row := offsiteCustomerRow{CustomerName: name, Dedicated: d.Type == "dedicated"}
		if d.Type == "shared" && d.QuotaGB > 0 {
			row.QuotaStr = fmt.Sprintf("%d GB", d.QuotaGB)
		}
		if hasReport[cfg.CustomerID] {
			row.UsageBytes = usage[cfg.CustomerID]
			row.UsageStr = fmtBytesGB(row.UsageBytes)
			if d.Type == "shared" && d.QuotaGB > 0 {
				row.UsagePercent = float64(row.UsageBytes) * 100 / float64(int64(d.QuotaGB)<<30)
				row.UsageBand = pctBand(row.UsagePercent, 90, 95) // matches the per-customer OffsiteChecker bands
				row.HasBar = true
			}
		} else {
			row.NoUsage = true
		}
		rows = append(rows, row)
	}
	sort.SliceStable(rows, func(i, j int) bool { return rows[i].UsageBytes > rows[j].UsageBytes })
	return rows
}
