package stacks

import (
	"fmt"
	"log"

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

// `data_paths:` — the customer-facing folder ANNOTATION (R-75).
//
// It annotates paths that ALREADY EXIST as compose binds; it can never declare a new one. That is
// what keeps it cheap: no new filesystem-write primitive is introduced from catalog data, because
// every path it names is a path the deploy belt already creates.

// DataPathRole is the presentation role of an annotated folder.
type DataPathRole string

const (
	RoleImport  DataPathRole = "import"  // a drop-zone: the app consumes and usually DELETES from it
	RoleLibrary DataPathRole = "library" // where the customer's files live
	RoleExport  DataPathRole = "export"  // the app writes results here for the customer to collect
)

// DataPath is one entry of the `data_paths:` block.
type DataPath struct {
	Path  string             `yaml:"path" json:"path"`   // relative to Root, must match a compose bind
	Root  appbackup.BindRoot `yaml:"root" json:"root"`   // import | userdata | hdd
	Role  DataPathRole       `yaml:"role" json:"role"`   // import | library | export
	Label string             `yaml:"label" json:"label"` // Hungarian, customer-facing
}

// validRole reports whether r is a role this controller knows how to render.
func validRole(r DataPathRole) bool {
	switch r {
	case RoleImport, RoleLibrary, RoleExport:
		return true
	default:
		return false
	}
}

// ValidateDataPaths applies the Fork-3 ruling, and the ASYMMETRY is deliberate — a decision, not an
// inconsistency:
//
//   - A MALFORMED PATH is a whole-block reject (returns an error; the caller drops the entire block
//     and the app renders no folder card). Paths govern DATA HANDLING: a path that is absolute,
//     escapes the root, or matches no compose bind would point the customer at a directory that is
//     not the app's, so nothing from that block can be trusted. This follows the `backup:` precedent
//     and reuses its refusal set (appbackup.ValidateRelPath) rather than defining a second one.
//
//   - An UNKNOWN ROLE fails OPEN (that one entry is dropped with a WARN; everything else renders).
//     Roles govern PRESENTATION only, and this follows the Lifecycle precedent (metadata.go): a typo
//     in a catalog push must never brick a template. The cost of an unknown role is one missing UI
//     affordance, never a mishandled file.
//
// Returns the surviving entries. An error means the caller must discard the WHOLE block.
func ValidateDataPaths(entries []DataPath, binds []appbackup.ComposeBind, appName string, logger *log.Logger) ([]DataPath, error) {
	if len(entries) == 0 {
		return nil, nil
	}
	present := make(map[appbackup.BindRoot]map[string]bool)
	for _, b := range binds {
		if present[b.Root] == nil {
			present[b.Root] = make(map[string]bool)
		}
		present[b.Root][b.RelPath] = true
	}

	seen := make(map[string]bool)
	out := make([]DataPath, 0, len(entries))
	for _, e := range entries {
		// --- path rules: whole-block reject ---
		if !appbackup.ValidRoot(e.Root) {
			return nil, fmt.Errorf("data_paths[%q]: unknown root %q (want import|userdata|hdd)", e.Path, e.Root)
		}
		if err := appbackup.ValidateRelPath(e.Root, e.Path); err != nil {
			return nil, fmt.Errorf("data_paths: %w", err)
		}
		key := string(e.Root) + "\x00" + e.Path
		if seen[key] {
			return nil, fmt.Errorf("data_paths[%s/%s]: duplicate entry", e.Root, e.Path)
		}
		seen[key] = true
		if !present[e.Root][e.Path] {
			return nil, fmt.Errorf("data_paths[%s/%s]: matches no compose bind — data_paths ANNOTATES existing binds, it cannot declare new ones", e.Root, e.Path)
		}
		// --- role: fail OPEN ---
		if !validRole(e.Role) {
			if logger != nil {
				logger.Printf("[WARN] [stacks] %s: unknown data_paths role %q for %s/%s — entry not surfaced (known: %s, %s, %s)",
					appName, e.Role, e.Root, e.Path, RoleImport, RoleLibrary, RoleExport)
			}
			continue
		}
		out = append(out, e)
	}
	return out, nil
}
