package handler

import (
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"gopkg.in/yaml.v3"

	"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.
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 — the dispatcher routes writes
//
//	(PUT/DELETE/POST) directly to ServeFileAPI.
//
// 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, the body is the cascade's
//
//	leaf-level ZddcFile (what internal/zddc/defaults/'s paths:
//	tree declares for THIS exact directory, plus any
//	virtual contributions threaded through by the walker)
//	marshalled as YAML. A header comment names the source
//	and points at ?effective=1 for the composed view. The
//	virtual body is itself valid YAML — PUT-saving it back
//	(with or without edits) through the file API
//	materialises a real on-disk override carrying exactly
//	the bytes the user saved. The response sets
//	X-ZDDC-Source: virtual:zddc so clients can distinguish.
func ServeZddcFile(cfg config.Config, w http.ResponseWriter, r *http.Request) {
	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.AllowFromChainP(r.Context(), decider, chain, PrincipalFromContext(r), dirURL); !allowed {
		http.NotFound(w, r) // hide existence from unauthorised callers
		return
	}

	// ?effective=1 branch: return the composed cascade view as JSON.
	// Distinct from the .zddc file itself — the YAML body is "what's
	// defined at this level" (source of truth); this is "what's
	// effective after merging every ancestor" (inspection only, not
	// PUT-saveable as a .zddc).
	if r.URL.Query().Get("effective") == "1" {
		serveEffectiveZddc(cfg, dirURL, chain, w, r)
		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 → render the cascade's leaf level as YAML.
	// What the user sees is the embedded defaults' declared shape
	// for this exact path; PUT-saving it back materialises an
	// on-disk override verbatim.
	body, err := renderVirtualZddc(chain)
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	w.Header().Set("X-ZDDC-Source", "virtual:zddc")
	if r.Method == http.MethodHead {
		return
	}
	_, _ = w.Write([]byte(body))
}

// renderVirtualZddc produces a YAML body for a directory that has no
// .zddc on disk. The body is the cascade's leaf-level ZddcFile —
// i.e. what the built-in defaults bundle (exportable/overridable as a
// root .zddc.zip via `zddc-server show-defaults`) declares for this
// exact directory, plus any contributions the walker threaded through.
// The goal is to expose the baseline's source of truth: a new
// user opening the virtual .zddc here sees, in the same yaml shape
// they would write themselves, what behavior is currently declared
// at this level. A header comment names the source and points at
// ?effective=1 for the composed view across the chain.
//
// PUT-saving these bytes back through the file API materialises a
// real on-disk override carrying exactly the saved content — the
// virtual body is a template, not a contract; the operator can
// trim / extend / overwrite freely.
func renderVirtualZddc(chain zddc.PolicyChain) (string, error) {
	var leaf zddc.ZddcFile
	if n := len(chain.Levels); n > 0 {
		leaf = chain.Levels[n-1]
	}

	var b strings.Builder
	b.WriteString("# Virtual .zddc — no file on disk at this directory.\n")
	b.WriteString("# The content below is what the policy baseline declares\n")
	b.WriteString("# for this exact path: the built-in defaults bundle — the\n")
	b.WriteString("# same one you can export, and override, as a root\n")
	b.WriteString("# .zddc.zip (`zddc-server show-defaults`) — with any\n")
	b.WriteString("# on-disk ancestor .zddc overrides already threaded in.\n")
	b.WriteString("# Edit and save through the YAML editor in browse to\n")
	b.WriteString("# materialise a real .zddc here carrying your changes;\n")
	b.WriteString("# the bytes you save become the override verbatim (no\n")
	b.WriteString("# merge, no synthesis — .zddc files drive policy and are\n")
	b.WriteString("# the single source of truth).\n")
	b.WriteString("#\n")
	b.WriteString("# For the COMPOSED effective config across the whole\n")
	b.WriteString("# cascade (all ancestors merged), query:\n")
	b.WriteString("#   GET <this-url>?effective=1   (JSON, not a .zddc).\n")

	if isZeroZddcFile(leaf) {
		b.WriteString("#\n")
		b.WriteString("# No rules declared at this exact level — every rule\n")
		b.WriteString("# currently in effect here is inherited from ancestors.\n")
		b.WriteString("{}\n")
		return b.String(), nil
	}

	body, err := yaml.Marshal(&leaf)
	if err != nil {
		return "", err
	}
	b.WriteByte('\n')
	b.Write(body)
	return b.String(), nil
}

// effectiveSourceView is the wire shape for one entry in the
// `sources` array of the ?effective=1 response. Level matches
// zddc.SourceEntry.Level (-1 = embedded baseline, 0+ = chain index);
// URL is the directory URL of that level (or "<embedded>" for the
// baseline); Contributed lists the top-level fields the level
// declared.
type effectiveSourceView struct {
	Level       int      `json:"level"`
	URL         string   `json:"url"`
	Contributed []string `json:"contributed,omitempty"`
}

// effectiveZddcView is the wire shape for the ?effective=1 response.
// Merged is the composed cascade as a ZddcFile (same struct shape the
// editor consumes for an on-disk .zddc; client-side renderers can
// reuse the same parser). Sources lists per-level contributions so
// the user can trace any value back to its origin without re-walking
// the cascade by hand.
type effectiveZddcView struct {
	URLPath string                `json:"url_path"`
	Merged  zddc.ZddcFile         `json:"merged"`
	Sources []effectiveSourceView `json:"sources"`
}

// serveEffectiveZddc writes the JSON composed-cascade view for the
// .zddc URL. Same ACL as the YAML view (already enforced by the
// caller).
func serveEffectiveZddc(cfg config.Config, dirURL string, chain zddc.PolicyChain, w http.ResponseWriter, r *http.Request) {
	merged, sources := zddc.EffectiveZddc(chain)
	levelURLs := levelURLsFor(cfg.Root, dirURL, len(chain.Levels))
	view := effectiveZddcView{
		URLPath: dirURL,
		Merged:  merged,
		Sources: make([]effectiveSourceView, 0, len(sources)),
	}
	for _, s := range sources {
		entry := effectiveSourceView{Level: s.Level, Contributed: s.Contributed}
		if s.Level < 0 {
			entry.URL = "<embedded>"
		} else if s.Level < len(levelURLs) {
			entry.URL = levelURLs[s.Level] + ".zddc"
		}
		view.Sources = append(view.Sources, entry)
	}
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("X-ZDDC-Source", "virtual:effective")
	if r.Method == http.MethodHead {
		return
	}
	writeJSON(w, view)
}

// levelURLsFor maps each chain level index to its directory URL. The
// chain walks dirs root→leaf so levelURLs[0] = "/", levelURLs[1] is
// the first segment, etc. Length must equal len(chain.Levels).
//
// Used by serveEffectiveZddc to populate SourceEntry.URL — clients
// receive concrete .zddc URLs they can navigate to rather than bare
// integer indices.
func levelURLsFor(_, dirURL string, n int) []string {
	dirURL = strings.TrimSuffix(dirURL, "/")
	out := make([]string, n)
	out[0] = "/"
	if dirURL == "" || n == 1 {
		return out
	}
	segs := strings.Split(strings.TrimPrefix(dirURL, "/"), "/")
	cur := ""
	for i, seg := range segs {
		if i+1 >= n {
			break
		}
		cur += "/" + seg
		out[i+1] = cur + "/"
	}
	return out
}

// isZeroZddcFile reports whether zf carries no declarations a user
// would want to see — every field is its zero value. Used to switch
// the virtual body between the rich path (marshal the leaf) and the
// empty-placeholder path (just say "nothing declared here").
//
// The ACL substruct's Inherit pointer being nil is part of "zero"
// here; an explicit inherit: false is itself a declaration worth
// surfacing.
func isZeroZddcFile(zf zddc.ZddcFile) bool {
	return zf.Title == "" &&
		zf.CreatedBy == "" &&
		zf.DefaultTool == "" &&
		zf.DirTool == "" &&
		zf.ReceivedPath == "" &&
		zf.PlannedReviewDate == "" &&
		zf.PlannedResponseDate == "" &&
		zf.ACL.Inherit == nil &&
		zf.AutoOwn == nil &&
		zf.AutoOwnFenced == nil &&
		zf.Virtual == nil &&
		zf.DropTarget == nil &&
		zf.Convert == nil &&
		len(zf.ACL.Permissions) == 0 &&
		len(zf.Admins) == 0 &&
		len(zf.Tables) == 0 &&
		len(zf.Views) == 0 &&
		len(zf.Display) == 0 &&
		len(zf.Roles) == 0 &&
		len(zf.FieldCodes) == 0 &&
		len(zf.Records) == 0 &&
		len(zf.AvailableTools) == 0 &&
		len(zf.Worm) == 0 &&
		len(zf.Paths) == 0
}
