package server

import "strings"

type fsListMode uint8

const (
	fsListDenied fsListMode = iota
	fsListProjected
	fsListFull
)

type fsListDecision struct {
	Mode              fsListMode
	Hidden            bool
	Boundary          bool
	projectedChildren map[string]struct{}
}

type fsStatMode uint8

const (
	fsStatDenied fsStatMode = iota
	fsStatProjected
	fsStatFull
)

type fsStatDecision struct {
	Mode             fsStatMode
	Hidden           bool
	HideNonDirectory bool
	Boundary         bool
}

type fsProjectionState struct {
	Visible           bool
	Hidden            bool
	Boundary          bool
	UnderListTarget   bool
	projectedChildren map[string]struct{}
}

func (s *TenantScope) resolveFSList(rawPath string) (fsListDecision, error) {
	if s == nil {
		return fsListDecision{}, ErrFSAccessDenied
	}
	if !s.IsScoped {
		return fsListDecision{Mode: fsListFull}, nil
	}
	p, err := normalizeFSAuthorizationPath(rawPath)
	if err != nil {
		return fsListDecision{}, err
	}
	for _, scope := range s.FSScopes {
		if scopeAllows(scope, FSOpList, p) {
			return fsListDecision{Mode: fsListFull}, nil
		}
	}

	projection := s.resolveFSProjection(p)
	if projection.Visible {
		return fsListDecision{
			Mode:              fsListProjected,
			Boundary:          projection.Boundary,
			projectedChildren: projection.projectedChildren,
		}, nil
	}
	return fsListDecision{Mode: fsListDenied, Hidden: projection.Hidden}, nil
}

func (s *TenantScope) resolveFSStat(rawPath string) (fsStatDecision, error) {
	if s == nil {
		return fsStatDecision{}, ErrFSAccessDenied
	}
	if !s.IsScoped {
		return fsStatDecision{Mode: fsStatFull}, nil
	}
	p, err := normalizeFSAuthorizationPath(rawPath)
	if err != nil {
		return fsStatDecision{}, err
	}
	for _, scope := range s.FSScopes {
		if scopeAllows(scope, FSOpRead, p) {
			return fsStatDecision{Mode: fsStatFull}, nil
		}
	}

	projection := s.resolveFSProjection(p)
	if projection.Visible {
		return fsStatDecision{
			Mode:             fsStatProjected,
			HideNonDirectory: !projection.UnderListTarget,
			Boundary:         projection.Boundary,
		}, nil
	}
	return fsStatDecision{Mode: fsStatDenied, Hidden: projection.Hidden}, nil
}

func (s *TenantScope) resolveFSProjection(p string) fsProjectionState {
	state := fsProjectionState{}
	underBoundary := false
	for _, pseudoRoot := range s.FSPseudoRoots {
		if !pathMatchesScope(pseudoRoot, p) {
			continue
		}
		underBoundary = true
		if p == pseudoRoot {
			state.Visible = true
			state.Boundary = true
		}
		for _, scope := range s.FSScopes {
			if !scope.Ops[FSOpList] || !pathMatchesScope(pseudoRoot, scope.Prefix) || scope.Prefix == pseudoRoot {
				continue
			}
			if pathMatchesScope(p, scope.Prefix) {
				state.Visible = true
				child := nextProjectedChild(p, scope.Prefix)
				if child != "" {
					if state.projectedChildren == nil {
						state.projectedChildren = make(map[string]struct{})
					}
					state.projectedChildren[child] = struct{}{}
				}
			}
			if scope.Prefix != p && pathMatchesScope(scope.Prefix, p) {
				state.Visible = true
				state.UnderListTarget = true
			}
		}
	}
	state.Hidden = underBoundary && !state.Visible
	return state
}

func nextProjectedChild(dir, target string) string {
	if dir == target {
		return ""
	}
	rel := ""
	if dir == "/" {
		rel = strings.TrimPrefix(target, "/")
	} else {
		prefix := dir + "/"
		if !strings.HasPrefix(target, prefix) {
			return ""
		}
		rel = strings.TrimPrefix(target, prefix)
	}
	if rel == "" {
		return ""
	}
	if slash := strings.IndexByte(rel, '/'); slash >= 0 {
		return rel[:slash]
	}
	return rel
}
