// Package panelacl implements per-panel ownership and sharing (Phase 2) plus a
// nested folder hierarchy for organising panels (Phase 3). It persists a single
// sidecar index, {storageDir}/acl.json, alongside the XML interface files.
//
// The effective permission a user has on a panel is the maximum of: ownership
// (always write), the panel's public level, any direct user grant, any grant to
// a user-group the user belongs to, and the permissions inherited from the
// folder chain the panel lives in. Callers are expected to further cap this by
// the user's global access level (see internal/access).
//
// Panels that have no record in the index are treated as fully open
// (PermWrite). This preserves access to interfaces created before ACLs existed;
// panels created through the ACL-aware API are recorded as private to their
// owner.
package panelacl

import (
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"sync"
)

// ErrNotFound is returned when a folder does not exist.
var ErrNotFound = errors.New("not found")

// Perm is an effective access level on a panel or folder.
type Perm int

const (
	// PermNone denies access.
	PermNone Perm = iota
	// PermRead permits viewing only.
	PermRead
	// PermWrite permits viewing and modification.
	PermWrite
)

// String renders a Perm as the token used in the JSON index and HTTP API.
func (p Perm) String() string {
	switch p {
	case PermRead:
		return "read"
	case PermWrite:
		return "write"
	default:
		return "none"
	}
}

// parsePerm maps a config/API token to a Perm. Unknown/empty → PermNone.
func parsePerm(s string) Perm {
	switch s {
	case "read":
		return PermRead
	case "write", "readwrite", "rw":
		return PermWrite
	default:
		return PermNone
	}
}

// Grant shares a panel or folder with a single user or a named user-group.
type Grant struct {
	Kind string `json:"kind"` // "user" | "group"
	Name string `json:"name"` // username or user-group name
	Perm string `json:"perm"` // "read" | "write"
}

// PanelACL is the ownership + sharing record for one panel.
type PanelACL struct {
	Owner  string  `json:"owner"`
	Folder string  `json:"folder,omitempty"` // folder id the panel belongs to ("" = root)
	Public string  `json:"public,omitempty"` // "" | "read" | "write"
	Grants []Grant `json:"grants,omitempty"`
	Order  float64 `json:"order,omitempty"` // sort position within its folder
}

// Folder is a node in the panel-organisation hierarchy. Permissions set on a
// folder are inherited by every panel and subfolder beneath it.
type Folder struct {
	ID     string  `json:"id"`
	Name   string  `json:"name"`
	Parent string  `json:"parent,omitempty"` // parent folder id ("" = root)
	Owner  string  `json:"owner"`
	Public string  `json:"public,omitempty"`
	Grants []Grant `json:"grants,omitempty"`
}

type index struct {
	Panels  map[string]*PanelACL `json:"panels"`
	Folders map[string]*Folder   `json:"folders"`
}

// Store persists and resolves the panel ACL index. It is safe for concurrent
// use.
type Store struct {
	path string
	mu   sync.RWMutex
	idx  index
}

// New opens (loading if present) the acl.json index in storageDir.
func New(storageDir string) (*Store, error) {
	s := &Store{
		path: filepath.Join(storageDir, "acl.json"),
		idx:  index{Panels: map[string]*PanelACL{}, Folders: map[string]*Folder{}},
	}
	data, err := os.ReadFile(s.path)
	if errors.Is(err, os.ErrNotExist) {
		return s, nil
	}
	if err != nil {
		return nil, err
	}
	if err := json.Unmarshal(data, &s.idx); err != nil {
		return nil, err
	}
	if s.idx.Panels == nil {
		s.idx.Panels = map[string]*PanelACL{}
	}
	if s.idx.Folders == nil {
		s.idx.Folders = map[string]*Folder{}
	}
	return s, nil
}

// saveLocked atomically persists the index. The caller must hold s.mu.
func (s *Store) saveLocked() error {
	data, err := json.MarshalIndent(s.idx, "", "  ")
	if err != nil {
		return err
	}
	tmp := s.path + ".tmp"
	if err := os.WriteFile(tmp, data, 0o644); err != nil {
		return err
	}
	return os.Rename(tmp, s.path)
}

// ── Panel records ────────────────────────────────────────────────────────────

// GetPanel returns a copy of a panel's ACL record, or nil if it is unmanaged.
func (s *Store) GetPanel(id string) *PanelACL {
	s.mu.RLock()
	defer s.mu.RUnlock()
	acl, ok := s.idx.Panels[id]
	if !ok {
		return nil
	}
	return clonePanel(acl)
}

// CreatePanel records ownership for a newly created panel, defaulting to
// private (owner-only) access. Existing records are left untouched.
func (s *Store) CreatePanel(id, owner string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.idx.Panels[id]; ok {
		return nil
	}
	s.idx.Panels[id] = &PanelACL{Owner: owner}
	return s.saveLocked()
}

// SetPanel replaces a panel's sharing settings (folder, public level, grants)
// while preserving its existing owner (or adopting the supplied owner when the
// panel was previously unmanaged).
func (s *Store) SetPanel(id, owner, folder, public string, grants []Grant) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	acl := s.idx.Panels[id]
	if acl == nil {
		acl = &PanelACL{Owner: owner}
		s.idx.Panels[id] = acl
	}
	acl.Folder = folder
	acl.Public = public
	acl.Grants = grants
	return s.saveLocked()
}

// PlacePanel sets only a panel's organizational fields — its folder and sort
// order — preserving ownership and sharing. A previously-unmanaged panel gets a
// record with no owner, which PanelPerm still treats as fully open (so dragging
// a legacy panel into a folder does not lock it).
func (s *Store) PlacePanel(id, folder string, order float64) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	acl := s.idx.Panels[id]
	if acl == nil {
		acl = &PanelACL{}
		s.idx.Panels[id] = acl
	}
	acl.Folder = folder
	acl.Order = order
	return s.saveLocked()
}

// DeletePanel removes a panel's ACL record (called when the panel is deleted).
func (s *Store) DeletePanel(id string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.idx.Panels[id]; !ok {
		return nil
	}
	delete(s.idx.Panels, id)
	return s.saveLocked()
}

// ── Folders ──────────────────────────────────────────────────────────────────

// Folders returns a copy of every folder, keyed by id.
func (s *Store) Folders() map[string]Folder {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make(map[string]Folder, len(s.idx.Folders))
	for id, f := range s.idx.Folders {
		out[id] = *cloneFolder(f)
	}
	return out
}

// GetFolder returns a copy of a folder, or ErrNotFound.
func (s *Store) GetFolder(id string) (Folder, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	f, ok := s.idx.Folders[id]
	if !ok {
		return Folder{}, ErrNotFound
	}
	return *cloneFolder(f), nil
}

// CreateFolder adds a new folder owned by owner and returns it.
func (s *Store) CreateFolder(id, name, parent, owner string) (Folder, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if parent != "" {
		if _, ok := s.idx.Folders[parent]; !ok {
			return Folder{}, ErrNotFound
		}
	}
	f := &Folder{ID: id, Name: name, Parent: parent, Owner: owner}
	s.idx.Folders[id] = f
	if err := s.saveLocked(); err != nil {
		return Folder{}, err
	}
	return *cloneFolder(f), nil
}

// UpdateFolder replaces a folder's mutable fields (name, parent, public,
// grants). It rejects parent changes that would create a cycle.
func (s *Store) UpdateFolder(id, name, parent, public string, grants []Grant) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	f, ok := s.idx.Folders[id]
	if !ok {
		return ErrNotFound
	}
	if parent != "" {
		if _, ok := s.idx.Folders[parent]; !ok {
			return ErrNotFound
		}
		if s.createsCycleLocked(id, parent) {
			return errors.New("folder parent change would create a cycle")
		}
	}
	f.Name = name
	f.Parent = parent
	f.Public = public
	f.Grants = grants
	return s.saveLocked()
}

// DeleteFolder removes a folder, reparenting its child folders and panels to the
// deleted folder's parent (so nothing is orphaned).
func (s *Store) DeleteFolder(id string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	f, ok := s.idx.Folders[id]
	if !ok {
		return ErrNotFound
	}
	newParent := f.Parent
	for _, child := range s.idx.Folders {
		if child.Parent == id {
			child.Parent = newParent
		}
	}
	for _, acl := range s.idx.Panels {
		if acl.Folder == id {
			acl.Folder = newParent
		}
	}
	delete(s.idx.Folders, id)
	return s.saveLocked()
}

// createsCycleLocked reports whether setting folder id's parent to newParent
// would introduce a cycle. The caller must hold s.mu.
func (s *Store) createsCycleLocked(id, newParent string) bool {
	for cur := newParent; cur != ""; {
		if cur == id {
			return true
		}
		f, ok := s.idx.Folders[cur]
		if !ok {
			return false
		}
		cur = f.Parent
	}
	return false
}

// ── Permission resolution ────────────────────────────────────────────────────

// PanelPerm returns the effective permission user has on panel id, given the
// user-groups the user belongs to. Unmanaged panels are fully open.
func (s *Store) PanelPerm(id, user string, groups []string) Perm {
	s.mu.RLock()
	defer s.mu.RUnlock()
	acl, ok := s.idx.Panels[id]
	if !ok {
		return PermWrite // legacy/unmanaged panel: fully open
	}
	// A record carrying only organizational metadata (folder/order) with no
	// owner, public level, or grants is still open — keeps an unmanaged panel
	// accessible after it has merely been moved into a folder or reordered.
	if acl.Owner == "" && acl.Public == "" && len(acl.Grants) == 0 {
		return PermWrite
	}
	best := PermNone
	if user != "" && acl.Owner == user {
		return PermWrite
	}
	best = maxPerm(best, parsePerm(acl.Public))
	best = maxPerm(best, grantsPerm(acl.Grants, user, groups))
	if acl.Folder != "" {
		best = maxPerm(best, s.folderPermLocked(acl.Folder, user, groups, map[string]bool{}))
	}
	return best
}

// FolderPerm returns the effective permission user has on folder id.
func (s *Store) FolderPerm(id, user string, groups []string) Perm {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.folderPermLocked(id, user, groups, map[string]bool{})
}

func (s *Store) folderPermLocked(id, user string, groups []string, seen map[string]bool) Perm {
	if id == "" || seen[id] {
		return PermNone
	}
	seen[id] = true
	f, ok := s.idx.Folders[id]
	if !ok {
		return PermNone
	}
	if user != "" && f.Owner == user {
		return PermWrite
	}
	best := PermNone
	best = maxPerm(best, parsePerm(f.Public))
	best = maxPerm(best, grantsPerm(f.Grants, user, groups))
	best = maxPerm(best, s.folderPermLocked(f.Parent, user, groups, seen))
	return best
}

func grantsPerm(grants []Grant, user string, groups []string) Perm {
	best := PermNone
	for _, g := range grants {
		switch g.Kind {
		case "user":
			if user != "" && g.Name == user {
				best = maxPerm(best, parsePerm(g.Perm))
			}
		case "group":
			if contains(groups, g.Name) {
				best = maxPerm(best, parsePerm(g.Perm))
			}
		}
	}
	return best
}

// ── helpers ──────────────────────────────────────────────────────────────────

func maxPerm(a, b Perm) Perm {
	if a > b {
		return a
	}
	return b
}

func contains(xs []string, v string) bool {
	for _, x := range xs {
		if x == v {
			return true
		}
	}
	return false
}

func clonePanel(a *PanelACL) *PanelACL {
	c := *a
	c.Grants = append([]Grant(nil), a.Grants...)
	return &c
}

func cloneFolder(f *Folder) *Folder {
	c := *f
	c.Grants = append([]Grant(nil), f.Grants...)
	return &c
}
