package handler

import (
	"context"
	"log/slog"
	"os"
	"path/filepath"
	"strings"

	"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
	"codeberg.org/VARASYS/ZDDC/zddc/internal/policy"
	"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)

// ProjectInfo is a single entry in the EnumerateProjects result.
// Used by the profile page (server-rendered) to list projects the
// caller can reach. NOT the wire shape for GET / Accept: application/json
// anymore — that endpoint now returns listing.FileInfo entries with
// per-entry Title, matching every other directory listing.
//
// Title is read from the project's own .zddc file (its `title:` field) when
// present; absent or empty means the profile page shows just the directory
// name. omitempty keeps the JSON small for projects without titles.
type ProjectInfo struct {
	Name  string `json:"name"`
	URL   string `json:"url"`
	Title string `json:"title,omitempty"`
}

// EnumerateProjects returns the visible top-level projects for the given
// caller. Exported for the profile page's server-rendered project list.
// A nil decider falls back to the internal Go evaluator.
func EnumerateProjects(ctx context.Context, decider policy.Decider, cfg config.Config, p zddc.Principal) ([]ProjectInfo, error) {
	if decider == nil {
		decider = &policy.InternalDecider{}
	}
	entries, err := os.ReadDir(cfg.Root)
	if err != nil {
		slog.Error("reading root directory", "err", err)
		return nil, err
	}

	var projects []ProjectInfo
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		name := entry.Name()
		// Skip hidden directories. Both '.' and '_' are reserved prefixes:
		// '.' for system/internal state (matches the listing-pipeline filter
		// and the dispatch dot-prefix guard); '_' for operator-managed
		// scaffolding like install.zip's _template/ directory that should
		// be reachable by direct URL but not appear in the project picker.
		if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
			continue
		}
		absPath := filepath.Join(cfg.Root, name)
		chain, err := zddc.EffectivePolicy(cfg.Root, absPath)
		if err != nil {
			slog.Warn("ACL policy error", "path", absPath, "err", err)
		}
		if allowed, _ := policy.AllowFromChainP(ctx, decider, chain, p, "/"+name+"/"); !allowed {
			continue
		}
		// Title comes from <project>/.zddc — optional, ignored on parse error.
		var title string
		if zf, err := zddc.ParseFile(filepath.Join(absPath, ".zddc")); err == nil {
			title = zf.Title
		}
		projects = append(projects, ProjectInfo{
			Name:  name,
			URL:   "/" + name + "/",
			Title: title,
		})
	}
	return projects, nil
}
