package settings

import (
	"fmt"
	"regexp"
	"strings"
	"time"
)

// LAN network-sharing (Samba) settings — R-7 slice 1. The household SMB password is NEVER persisted
// here (it lives in the samba container's passdb volume); SMBSettings.UserSet only records that one
// exists. Everything in this file is customer-modifiable state behind the „Megosztás” page.

// DefaultSMBServerName is the NetBIOS name shown in Windows Explorer's Network view when the customer
// hasn't chosen one. Kept short + uppercase (NetBIOS is case-folded).
const DefaultSMBServerName = "FELHOM"

// SMBSettings holds the network-sharing feature toggle + server identity.
type SMBSettings struct {
	Enabled    bool   `json:"enabled"`
	ServerName string `json:"server_name"`        // NetBIOS name (≤15, NetBIOS-safe); "" ⇒ DefaultSMBServerName
	UserSet    bool   `json:"user_set,omitempty"` // the household SMB password has been set at least once
}

// SMBShare is one exported folder. Path is an absolute host path under a registered storage root
// (validated by the web layer against the storage registry + deny-list before it ever reaches here).
type SMBShare struct {
	Name      string `json:"name"`                // share name (NetBIOS-safe, ≤15); the [section] in smb.conf and the \\SERVER\<name> path
	Path      string `json:"path"`                // absolute host path
	ReadOnly  bool   `json:"read_only,omitempty"` // smb.conf `read only = yes` + a :ro compose bind
	Offsite   bool   `json:"offsite"`             // [R4] true (default) → backup class mandatory; false → optional (tier-2 only)
	CreatedAt string `json:"created_at"`          // RFC3339
}

// nbNameRe matches a NetBIOS-safe name: 1–15 chars, letters/digits/hyphen/underscore, not starting
// or ending with a hyphen. Deliberately stricter than SMB share-name rules (slice 1 keeps the flat
// name and the share name in the same safe space; slice 2 may relax share names).
var nbNameRe = regexp.MustCompile(`^[A-Za-z0-9_](?:[A-Za-z0-9_-]{0,13}[A-Za-z0-9_])?$`)

// ValidateSMBServerName checks a proposed server (NetBIOS) name, returning a Hungarian error on defect.
func ValidateSMBServerName(name string) error {
	name = strings.TrimSpace(name)
	if name == "" {
		return fmt.Errorf("a kiszolgáló neve nem lehet üres")
	}
	if len(name) > 15 {
		return fmt.Errorf("a kiszolgáló neve legfeljebb 15 karakter lehet")
	}
	if !nbNameRe.MatchString(name) {
		return fmt.Errorf("a kiszolgáló neve csak betűt, számot, kötőjelet és aláhúzást tartalmazhat")
	}
	return nil
}

// ValidateSMBShareName checks a proposed share name, returning a Hungarian error on defect. It rejects
// path traversal (slashes/backslashes/dots), whitespace, over-length, and any non-NetBIOS-safe char —
// so a name can never turn into a path segment or a second [section] header.
func ValidateSMBShareName(name string) error {
	name = strings.TrimSpace(name)
	if name == "" {
		return fmt.Errorf("a megosztás neve nem lehet üres")
	}
	if len(name) > 15 {
		return fmt.Errorf("a megosztás neve legfeljebb 15 karakter lehet")
	}
	if strings.ContainsAny(name, `/\.`) {
		return fmt.Errorf("a megosztás neve nem tartalmazhat perjelet vagy pontot")
	}
	// RESERVED NAMESPACE (R-7b). The backup engines key the shares source by the pseudo-stack „_shares"
	// — a restic tag, a tier-2 dest root and a status record. nbNameRe below starts with [A-Za-z0-9_],
	// so before this guard „_shares" was an ACCEPTED share name and the underscore namespace was not in
	// fact reserved (the R-7b task's assumption to the contrary was verified false here). Reserving the
	// whole leading-underscore space keeps future system keys collision-free too. Validation runs on
	// ADD only, so an already-registered share is never invalidated retroactively.
	if strings.HasPrefix(name, "_") {
		return fmt.Errorf("a megosztás neve nem kezdődhet aláhúzással — ezek a nevek a rendszernek vannak fenntartva")
	}
	if !nbNameRe.MatchString(name) {
		return fmt.Errorf("a megosztás neve csak betűt, számot, kötőjelet és aláhúzást tartalmazhat")
	}
	return nil
}

// EffectiveServerName returns the configured server name or the default when unset.
func (s *SMBSettings) EffectiveServerName() string {
	if s == nil || strings.TrimSpace(s.ServerName) == "" {
		return DefaultSMBServerName
	}
	return s.ServerName
}

// ---- accessors (thread-safe, mirror the OffboxTarget getter/setter shape) --------------------------

// GetSMBSettings returns a copy of the SMB feature settings (never nil; a zero-value disabled struct
// with the default server name when unconfigured).
func (s *Settings) GetSMBSettings() SMBSettings {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if s.SMB == nil {
		return SMBSettings{Enabled: false, ServerName: DefaultSMBServerName}
	}
	cp := *s.SMB
	if strings.TrimSpace(cp.ServerName) == "" {
		cp.ServerName = DefaultSMBServerName
	}
	return cp
}

// SetSMBEnabled toggles the feature and saves.
func (s *Settings) SetSMBEnabled(on bool) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.SMB == nil {
		s.SMB = &SMBSettings{ServerName: DefaultSMBServerName}
	}
	s.SMB.Enabled = on
	return s.save()
}

// SetSMBServerName updates the server (NetBIOS) name and saves. Caller validates.
func (s *Settings) SetSMBServerName(name string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.SMB == nil {
		s.SMB = &SMBSettings{}
	}
	s.SMB.ServerName = strings.TrimSpace(name)
	return s.save()
}

// SetSMBUserSet records that the household SMB password has been set at least once.
func (s *Settings) SetSMBUserSet(set bool) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.SMB == nil {
		s.SMB = &SMBSettings{ServerName: DefaultSMBServerName}
	}
	s.SMB.UserSet = set
	return s.save()
}

// GetSMBShares returns a copy of the share registry.
func (s *Settings) GetSMBShares() []SMBShare {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if len(s.SMBShares) == 0 {
		return nil
	}
	out := make([]SMBShare, len(s.SMBShares))
	copy(out, s.SMBShares)
	return out
}

// AddSMBShare appends a share, refusing a case-insensitive name collision. Caller has already
// validated the name (ValidateSMBShareName) and the path (against the storage registry + deny-list).
func (s *Settings) AddSMBShare(share SMBShare) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	for _, ex := range s.SMBShares {
		if strings.EqualFold(ex.Name, share.Name) {
			return fmt.Errorf("már létezik „%s” nevű megosztás", share.Name)
		}
	}
	if share.CreatedAt == "" {
		share.CreatedAt = time.Now().UTC().Format(time.RFC3339)
	}
	s.SMBShares = append(s.SMBShares, share)
	if s.log != nil {
		s.log.Printf("[INFO] [settings] Added SMB share: %s", share.Name)
	}
	return s.save()
}

// RemoveSMBShare deletes a share by name (case-insensitive). Config-only: never touches the folder.
func (s *Settings) RemoveSMBShare(name string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	var kept []SMBShare
	found := false
	for _, ex := range s.SMBShares {
		if strings.EqualFold(ex.Name, name) {
			found = true
			continue
		}
		kept = append(kept, ex)
	}
	if !found {
		return fmt.Errorf("nincs „%s” nevű megosztás", name)
	}
	s.SMBShares = kept
	if s.log != nil {
		s.log.Printf("[INFO] [settings] Removed SMB share: %s", name)
	}
	return s.save()
}

// SetSMBShareOffsite flips a share's „Felhőmentés” (offsite/mandatory) toggle [R4].
func (s *Settings) SetSMBShareOffsite(name string, offsite bool) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	for i := range s.SMBShares {
		if strings.EqualFold(s.SMBShares[i].Name, name) {
			s.SMBShares[i].Offsite = offsite
			return s.save()
		}
	}
	return fmt.Errorf("nincs „%s” nevű megosztás", name)
}
