package zddc

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
)

// ResolveCanonicalPath substitutes on-disk casing for any canonical
// ancestor segment of target, without creating anything. Returns target
// unchanged when no case variant exists or when target is at fsRoot or
// outside it.
//
// Use this before authorization to make ACL lookups operate against the
// real on-disk path without side effects. EnsureCanonicalAncestors
// performs the same substitution AND creates missing ancestors —
// authorization should run between the two.
func ResolveCanonicalPath(fsRoot, target string) (string, error) {
	rel, err := filepath.Rel(fsRoot, target)
	if err != nil {
		return target, err
	}
	rel = filepath.ToSlash(rel)
	if rel == "." || strings.HasPrefix(rel, "../") || rel == ".." {
		return target, nil
	}
	parts := strings.Split(rel, "/")
	resolvedSegs := make([]string, len(parts))
	copy(resolvedSegs, parts)

	join := func(n int) string {
		segs := append([]string{fsRoot}, resolvedSegs[:n]...)
		return filepath.Join(segs...)
	}
	resolveAt := func(n int, logical string) error {
		parent := join(n)
		if _, err := os.Stat(filepath.Join(parent, resolvedSegs[n])); err == nil {
			return nil
		}
		actual, err := ResolveCanonical(parent, logical)
		if err != nil {
			return err
		}
		if actual != "" {
			resolvedSegs[n] = actual
		}
		return nil
	}

	if len(parts) >= 2 {
		seg := strings.ToLower(parts[1])
		if seg == "archive" || seg == "working" || seg == "staging" {
			if err := resolveAt(1, seg); err != nil {
				return target, err
			}
		}
	}
	if len(parts) >= 4 && strings.EqualFold(resolvedSegs[1], "archive") {
		seg := strings.ToLower(parts[3])
		switch seg {
		case "mdl", "incoming", "received", "issued":
			if err := resolveAt(3, seg); err != nil {
				return target, err
			}
		}
	}
	return filepath.Join(append([]string{fsRoot}, resolvedSegs...)...), nil
}

// EnsureCanonicalAncestors walks from fsRoot down to filepath.Dir(target),
// creating any missing canonical-folder ancestor with MkdirAll(perm). For
// freshly-created auto-own ancestors (working/, staging/, or
// archive/<party>/incoming/), it also writes a creator-owned .zddc using
// principalEmail (skipped if principalEmail is empty).
//
// Returns the resolved version of target with on-disk casing substituted
// for any canonical ancestor whose disk variant differs from the requested
// casing — so a pre-existing Working/ is reused rather than shadowed by a
// new working/ sibling. The basename of target is never altered.
//
// Canonical positions, relative to fsRoot:
//
//   - <project>/<canonical-root> where <canonical-root> ∈
//     {archive, working, staging}
//   - <project>/archive/<party>/<canonical-party> where
//     <canonical-party> ∈ {mdl, incoming, received, issued}
//
// "reviewing" is intentionally NOT created here — it's a purely virtual
// route. A write that targets a path under <project>/reviewing/ returns
// an error (callers should reject before invoking this helper).
//
// fsRoot and target must be absolute filesystem paths under the same
// volume; target may not yet exist on disk.
func EnsureCanonicalAncestors(fsRoot, target, principalEmail string, perm fs.FileMode) (string, error) {
	rel, err := filepath.Rel(fsRoot, target)
	if err != nil {
		return target, fmt.Errorf("rel: %w", err)
	}
	rel = filepath.ToSlash(rel)
	if rel == "." || strings.HasPrefix(rel, "../") || rel == ".." {
		return target, fmt.Errorf("target %q escapes fsRoot %q", target, fsRoot)
	}

	parts := strings.Split(rel, "/")
	if len(parts) < 2 {
		// target is at fsRoot/<single-segment>; no canonical ancestors apply.
		return target, nil
	}

	// Reject writes under reviewing/ — virtual route.
	if len(parts) >= 2 && strings.EqualFold(parts[1], "reviewing") {
		return target, fmt.Errorf("reviewing/ is virtual and not writable")
	}

	resolvedSegs := make([]string, len(parts))
	copy(resolvedSegs, parts)

	// Track which ancestor directories we end up creating so we can seed
	// auto-own .zddc files on the right ones afterwards.
	type created struct {
		absPath string
		autoOwn bool
		fenced  bool
	}
	var freshlyCreated []created

	// joinUnder builds an absolute path from fsRoot + the first n resolved
	// segments.
	joinUnder := func(n int) string {
		segs := append([]string{fsRoot}, resolvedSegs[:n]...)
		return filepath.Join(segs...)
	}

	// resolveAt(n) tries to use the on-disk casing for resolvedSegs[n] inside
	// joinUnder(n), substituting if a case-variant directory exists.
	resolveAt := func(n int, logical string) error {
		parent := joinUnder(n)
		// Only substitute if the requested segment doesn't already match
		// on disk (cheap optimisation to avoid a ReadDir on the hot path).
		if _, err := os.Stat(filepath.Join(parent, resolvedSegs[n])); err == nil {
			return nil
		}
		actual, err := ResolveCanonical(parent, logical)
		if err != nil {
			return err
		}
		if actual != "" {
			resolvedSegs[n] = actual
		}
		return nil
	}

	// Walk depth 1 (project) → deeper levels, resolving + tracking as we go.
	// Depth 0 is the project segment; not a canonical name.
	if len(parts) >= 2 {
		// Depth 1 candidate: archive / working / staging.
		seg := strings.ToLower(parts[1])
		if seg == "archive" || seg == "working" || seg == "staging" {
			if err := resolveAt(1, seg); err != nil {
				return target, err
			}
		}
	}

	// Depth 3 candidate (archive/<party>/<canonical-party>): mdl / incoming /
	// received / issued. Only meaningful when depth 1 is "archive".
	if len(parts) >= 4 && strings.EqualFold(resolvedSegs[1], "archive") {
		seg := strings.ToLower(parts[3])
		switch seg {
		case "mdl", "incoming", "received", "issued":
			if err := resolveAt(3, seg); err != nil {
				return target, err
			}
		}
	}

	// Now create any missing ancestors. We MkdirAll up to (but not
	// including) the basename. The handler's actual write call still does
	// its own parent-dir creation; this is the proactive seeding pass.
	parentDir := filepath.Dir(filepath.Join(append([]string{fsRoot}, resolvedSegs...)...))
	rootRel, _ := filepath.Rel(fsRoot, parentDir)
	rootRel = filepath.ToSlash(rootRel)
	if rootRel == "." || strings.HasPrefix(rootRel, "../") {
		return filepath.Join(append([]string{fsRoot}, resolvedSegs...)...), nil
	}

	// Walk segment-by-segment. For each ancestor that doesn't exist yet,
	// create it and record whether the position is auto-own.
	pathSoFar := fsRoot
	parentSegs := strings.Split(rootRel, "/")
	for i, name := range parentSegs {
		pathSoFar = filepath.Join(pathSoFar, name)
		if info, err := os.Stat(pathSoFar); err == nil {
			if !info.IsDir() {
				return target, fmt.Errorf("ancestor %q exists but is not a directory", pathSoFar)
			}
			continue
		} else if !os.IsNotExist(err) {
			return target, err
		}

		if err := os.MkdirAll(pathSoFar, perm); err != nil {
			return target, err
		}

		// Determine if this newly-created ancestor is an auto-own position
		// and whether it should be fenced (inherit: false). 'i' is the
		// index into parentSegs (parentSegs[0] is the project segment).
		autoOwn, fenced := autoOwnDepthMatch(parentSegs, i)
		freshlyCreated = append(freshlyCreated, created{
			absPath: pathSoFar,
			autoOwn: autoOwn,
			fenced:  fenced,
		})
	}

	// Seed auto-own .zddc on the canonical positions that were freshly
	// created. Skip if no principal email is available (anonymous or
	// system writes). The fenced variant is used at per-user home
	// folders under working/ — private by default; owner can later
	// edit the .zddc to add collaborators.
	if principalEmail != "" {
		for _, c := range freshlyCreated {
			if !c.autoOwn {
				continue
			}
			var werr error
			if c.fenced {
				werr = WriteAutoOwnZddcFenced(c.absPath, principalEmail)
			} else {
				werr = WriteAutoOwnZddc(c.absPath, principalEmail)
			}
			if werr != nil {
				return target, fmt.Errorf("auto-own .zddc at %q: %w", c.absPath, werr)
			}
		}
	}

	return filepath.Join(append([]string{fsRoot}, resolvedSegs...)...), nil
}

// autoOwnDepthMatch reports whether parentSegs[idx] sits at a canonical
// auto-own depth, and whether the auto-own .zddc should fence ancestor
// cascade (inherit: false). parentSegs is the slash-relative path from
// project root onward (parentSegs[0] is the project segment).
//
// Per-user home folders at <project>/working/<email>/ are fenced so
// each user's working subtree is private by default; the user can edit
// the file to grant access to others. Other auto-own positions stay
// unfenced — they grant the creator while still allowing ancestor
// cascade to add (e.g.) admin grants from the project root.
func autoOwnDepthMatch(parentSegs []string, idx int) (autoOwn bool, fenced bool) {
	switch idx {
	case 1:
		// <project>/working or <project>/staging
		if strings.EqualFold(parentSegs[1], "working") || strings.EqualFold(parentSegs[1], "staging") {
			return true, false
		}
	case 2:
		// <project>/working/<segment> — per-user home folder. Fenced so
		// other users can't read it via ancestor cascade by default.
		if strings.EqualFold(parentSegs[1], "working") {
			return true, true
		}
	case 3:
		// <project>/archive/<party>/incoming
		if strings.EqualFold(parentSegs[1], "archive") && strings.EqualFold(parentSegs[3], "incoming") {
			return true, false
		}
	}
	return false, false
}

// isAutoOwnDepthMatch is a thin wrapper over autoOwnDepthMatch retained
// for any internal callers that only need the first bool.
func isAutoOwnDepthMatch(parentSegs []string, idx int) bool {
	autoOwn, _ := autoOwnDepthMatch(parentSegs, idx)
	return autoOwn
}
