package handler

import (
	"fmt"
	"net/http"
	"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"
)

// ZddcFileBasename is the leaf the dispatcher recognises as a raw
// .zddc YAML view request. Carved out of the dot-prefix guard so any
// directory's .zddc is reachable at <dir>/.zddc — without it, the
// dispatcher 404s anything beginning with a dot.
const ZddcFileBasename = ".zddc"

// IsZddcFileRequest reports whether urlPath ends with the raw .zddc
// leaf. Used by the dispatcher to route a GET/HEAD to ServeZddcFile
// before the dot-prefix guard rejects it.
//
// Excludes the `.zddc.html` editor leaf, which is handled by
// IsZddcEditorRequest / ServeZddcEditorAtPath.
func IsZddcFileRequest(urlPath string) bool {
	clean := strings.TrimSuffix(urlPath, "/")
	return strings.HasSuffix(clean, "/"+ZddcFileBasename) ||
		clean == "/"+ZddcFileBasename
}

// ServeZddcFile serves a directory's .zddc as a plain YAML view.
//
// Method:    GET / HEAD only; everything else → 405 with the existing
//            /.profile/zddc editor pointed to in the body.
// ACL:       the parent directory's read permission gates access. A
//            user who can read the directory can read its .zddc.
// On-disk:   if <dir>/.zddc exists, its bytes are returned verbatim
//            with Content-Type: application/yaml.
// Virtual:   if it does not exist, a synthetic body is returned with a
//            cascade summary so the operator can see what rules are
//            effective at this depth. The synthetic body is clearly
//            marked with comments — saving it via the editor (`<dir>/.zddc.html`)
//            materialises a real file. The virtual response sets
//            X-ZDDC-Source: virtual so the client can distinguish.
func ServeZddcFile(cfg config.Config, w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet && r.Method != http.MethodHead {
		w.Header().Set("Allow", "GET, HEAD")
		http.Error(w,
			"Method Not Allowed — .zddc is read-only via this URL.\n"+
				"To edit, open <dir>/.zddc.html (form-based editor, admin-only).\n",
			http.StatusMethodNotAllowed)
		return
	}
	email := EmailFromContext(r)
	decider := DeciderFromContext(r)

	// URL is <dir>/.zddc. Strip the leaf to get the directory.
	urlPath := r.URL.Path
	leaf := "/" + ZddcFileBasename
	if !strings.HasSuffix(urlPath, leaf) {
		http.NotFound(w, r)
		return
	}
	dirURL := strings.TrimSuffix(urlPath, leaf)
	if dirURL == "" {
		dirURL = "/"
	}

	// Translate the URL into an absolute filesystem path. The parent
	// directory must exist on disk (with one exception: the root
	// itself, which always exists). We do NOT require the directory
	// to exist if it's a canonical virtual folder — the cascade is
	// still defined for those paths via the ancestors.
	rel := strings.Trim(dirURL, "/")
	abs := cfg.Root
	if rel != "" {
		abs = filepath.Join(cfg.Root, filepath.FromSlash(rel))
		if !strings.HasPrefix(abs, cfg.Root+string(filepath.Separator)) {
			http.NotFound(w, r)
			return
		}
	}

	// ACL gate: read permission on the parent directory. We resolve
	// against the directory's effective policy chain, not the .zddc
	// file's own permissions (the file isn't a separate ACL target —
	// it's the source of the rules themselves).
	chain, err := zddc.EffectivePolicy(cfg.Root, abs)
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	if allowed, _ := policy.AllowFromChain(r.Context(), decider, chain, email, dirURL); !allowed {
		http.NotFound(w, r) // hide existence from unauthorised callers
		return
	}

	zddcPath := filepath.Join(abs, ".zddc")
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("Content-Type", "application/yaml; charset=utf-8")

	// On-disk file: serve bytes verbatim.
	if data, err := os.ReadFile(zddcPath); err == nil {
		w.Header().Set("X-ZDDC-Source", "file:"+filepath.ToSlash(strings.TrimPrefix(zddcPath, cfg.Root)))
		if r.Method == http.MethodHead {
			return
		}
		_, _ = w.Write(data)
		return
	} else if !os.IsNotExist(err) {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	// No file on disk → synthetic placeholder body with a cascade
	// summary so the user can see what's actually effective here.
	body := renderVirtualZddc(cfg.Root, abs, chain)
	w.Header().Set("X-ZDDC-Source", "virtual:zddc")
	if r.Method == http.MethodHead {
		return
	}
	_, _ = w.Write([]byte(body))
}

// renderVirtualZddc produces a self-describing YAML placeholder for a
// directory that has no .zddc on disk. The body is valid YAML (parses
// to an empty document) so a downstream YAML tool isn't fazed; the
// commentary lives in comments. Each ancestor's contribution is
// summarised so the reader sees exactly what's effective at this
// depth.
func renderVirtualZddc(fsRoot, dirAbs string, chain zddc.PolicyChain) string {
	var b strings.Builder
	fmt.Fprintf(&b, "# Virtual .zddc — no file on disk at this directory yet.\n")
	fmt.Fprintf(&b, "# Rules below are inherited from ancestors. To override at\n")
	fmt.Fprintf(&b, "# this level, edit via the form editor at <dir>/.zddc.html\n")
	fmt.Fprintf(&b, "# (admin-only). Saving creates a real file here.\n")
	fmt.Fprintf(&b, "#\n")
	fmt.Fprintf(&b, "# Effective cascade at %s:\n", urlPathOf(fsRoot, dirAbs))

	// Walk the levels from root down. Each ZddcFile in chain.Levels
	// corresponds to one ancestor (root, .../, ..., dirAbs). Show only
	// the levels that contributed something non-empty.
	dirs := chainDirs(fsRoot, dirAbs)
	any := false
	for i, lvl := range chain.Levels {
		var levelDir string
		if i < len(dirs) {
			levelDir = dirs[i]
		} else {
			levelDir = fsRoot
		}
		entry := summariseLevel(lvl)
		if entry == "" {
			continue
		}
		any = true
		fmt.Fprintf(&b, "#\n# from %s/.zddc:\n%s",
			urlPathOf(fsRoot, levelDir), entry)
	}
	if !any {
		fmt.Fprintf(&b, "#   (no ancestor .zddc contributes any rule)\n")
	}
	fmt.Fprintf(&b, "\n# --- placeholder body (empty) ---\n")
	fmt.Fprintf(&b, "{}\n")
	return b.String()
}

// summariseLevel produces a comment block describing one .zddc level's
// non-empty contributions (title, acl, admins, apps, tables). Empty
// levels return "" so the caller can skip them.
func summariseLevel(lvl zddc.ZddcFile) string {
	var b strings.Builder
	if lvl.Title != "" {
		fmt.Fprintf(&b, "#   title: %q\n", lvl.Title)
	}
	if len(lvl.ACL.Allow) > 0 {
		fmt.Fprintf(&b, "#   acl.allow: %v\n", lvl.ACL.Allow)
	}
	if len(lvl.ACL.Deny) > 0 {
		fmt.Fprintf(&b, "#   acl.deny:  %v\n", lvl.ACL.Deny)
	}
	if len(lvl.ACL.Permissions) > 0 {
		fmt.Fprintf(&b, "#   acl.permissions: %v\n", lvl.ACL.Permissions)
	}
	if len(lvl.Admins) > 0 {
		fmt.Fprintf(&b, "#   admins: %v\n", lvl.Admins)
	}
	if len(lvl.Apps) > 0 {
		fmt.Fprintf(&b, "#   apps:\n")
		for k, v := range lvl.Apps {
			fmt.Fprintf(&b, "#     %s: %s\n", k, v)
		}
	}
	if len(lvl.Tables) > 0 {
		fmt.Fprintf(&b, "#   tables:\n")
		for k, v := range lvl.Tables {
			fmt.Fprintf(&b, "#     %s: %s\n", k, v)
		}
	}
	return b.String()
}
