package web

import (
	"fmt"
	"path/filepath"

	"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
	"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
	"gitea.dooplex.hu/admin/felhom-controller/internal/system"
)

// „Hova tegyem a fájlokat?" — the app-page folder card (R-75).
//
// Rendered only for DEPLOYED apps that declare data_paths (Fork-2: all catalog apps get the
// skeleton, but only deployed apps get a UI affordance — an empty folder for an app nobody installed
// is the thing that would actually confuse someone).

// DataPathCard is one folder row on the app page.
type DataPathCard struct {
	Label       string // the catalog's Hungarian label
	Link        string // FileBrowser deep link
	Consequence string // class-DRIVEN copy (never hand-written per app)
	IsImport    bool   // drives the free-space line
	FreeSpace   string // system-drive headroom, import rows only ("" when unreadable)
}

// consequenceFor maps a folder's DERIVED BACKUP CLASS to the sentence the customer reads (Fork-4).
//
// It is driven by the class, not by the role and not by a per-app string, so the promise the UI makes
// can never drift from what the backup engines actually do. `excluded` means the tier filter drops it
// at EVERY tier — so a drop-zone must say, in the customer's own language, that the folder is
// temporary and unbacked. Saying anything softer would be a false promise about their files.
func consequenceFor(class appbackup.BindClass, role stacks.DataPathRole) string {
	switch class {
	case appbackup.ClassExcluded:
		if role == stacks.RoleImport {
			return "Ide másold a feldolgozandó fájlokat. Az alkalmazás beolvassa, majd törli innen — " +
				"ez a mappa átmeneti, és nem készül róla biztonsági mentés."
		}
		return "Ez a mappa átmeneti, és nem készül róla biztonsági mentés."
	case appbackup.ClassMandatory, appbackup.ClassOptional:
		return "Itt tárolódnak a fájljaid. Biztonsági mentés készül róla."
	default:
		// No classification (legacy app, or a bind with no backup block). Say nothing rather than
		// guess — an unverified backup promise is worse than no sentence at all.
		return ""
	}
}

// buildDataPathCards turns an app's validated data_paths into rendered rows.
//
// classOf resolves a (root, relpath) to its backup class; missing ⇒ empty class ⇒ no consequence
// line. A row whose deep link cannot be built (no domain) is dropped rather than rendered dead.
func (s *Server) buildDataPathCards(st *stacks.Stack) []DataPathCard {
	if st == nil || !st.Deployed || len(st.Meta.DataPaths) == 0 {
		return nil
	}
	domain := s.cfg.Customer.Domain
	if domain == "" {
		return nil
	}

	classOf := map[string]appbackup.BindClass{}
	if binds, has := s.stackMgr.ClassifiedBinds(st.Name); has {
		for _, b := range binds {
			classOf[string(b.Root)+"\x00"+b.RelPath] = b.Class
		}
	}

	var freeSpace string
	if s.stackMgr != nil {
		if root := s.stackMgr.GetImportRoot(); root != "" {
			if du := system.GetDiskUsage(root); du != nil {
				freeSpace = fmt.Sprintf("%.1f GB szabad", du.AvailGB)
			}
		}
	}

	cards := make([]DataPathCard, 0, len(st.Meta.DataPaths))
	for _, dp := range st.Meta.DataPaths {
		var link string
		switch dp.Root {
		case appbackup.RootImport:
			link = importFolderLink(domain, dp.Path)
		case appbackup.RootUserdata:
			// A userdata folder lives on the app's OWN drive, so its FileBrowser source is that
			// drive's sidebar entry. Resolve it from the app's HDD_PATH; skip the row if we cannot.
			src := s.fbSourceNameForApp(st.Name)
			if src == "" {
				continue
			}
			link = fileBrowserLink(domain, src, dp.Path)
		default:
			// hdd: app-internal (appdata/) — NOT customer-browsable, FileBrowser does not mount it.
			// Surfacing a link here would 404. Skipped deliberately; the catalog should not annotate
			// an hdd path with a customer-facing role.
			continue
		}
		isImport := dp.Role == stacks.RoleImport
		card := DataPathCard{
			Label:       dp.Label,
			Link:        link,
			Consequence: consequenceFor(classOf[string(dp.Root)+"\x00"+dp.Path], dp.Role),
			IsImport:    isImport,
		}
		if isImport {
			// The system SSD filling is a different severity from a data drive filling — it can take
			// the whole guest down — and the customer has no other signal that the drop-zone is not
			// bottomless.
			card.FreeSpace = freeSpace
		}
		cards = append(cards, card)
	}
	return cards
}

// fbSourceNameForApp returns the FileBrowser sidebar source name for the drive an app is deployed
// on: the storage path's label when it has one, else the mount basename — exactly what
// RenderFileBrowserConfig emits, so the deep link and the sidebar can never disagree.
func (s *Server) fbSourceNameForApp(stackName string) string {
	appCfg := s.stackMgr.LoadAppConfigByName(stackName)
	if appCfg == nil {
		return ""
	}
	hdd := appCfg.Env["HDD_PATH"]
	if hdd == "" {
		return ""
	}
	for _, sp := range s.settings.GetStoragePaths() {
		if sp.Path != hdd || sp.Decommissioned {
			continue
		}
		if sp.Label != "" {
			return sp.Label
		}
		return filepath.Base(sp.Path)
	}
	return ""
}
