package zddc

import (
	"strings"
)

// matchGlob looks up a path segment in a paths: map. Literal
// (case-insensitive) match first; falls back to a "*" segment-
// wildcard key if present. Returns nil when neither hits.
//
// Phase 2: single-segment globs only. Multi-segment keys (a/b) are
// rejected at the schema level and never reach this lookup.
func matchGlob(m map[string]ZddcFile, seg string) *ZddcFile {
	if m == nil {
		return nil
	}
	// Fast path: exact key match (case-sensitive — operator-controlled).
	if v, ok := m[seg]; ok {
		return &v
	}
	// Case-insensitive literal match for canonical-folder ergonomics
	// (operator writes `archive:`; on-disk dir may be `Archive`).
	lower := strings.ToLower(seg)
	for k, v := range m {
		if k == "*" {
			continue
		}
		if strings.ToLower(k) == lower {
			return &v
		}
	}
	// Wildcard fallback.
	if v, ok := m["*"]; ok {
		return &v
	}
	return nil
}

// mergeOverlay composes two ZddcFile values into one. `top` overrides
// `base` per-field. Maps merge key-by-key (top wins on key clash);
// scalar fields take top's value when non-zero (allowing base to fill
// in unset fields).
//
// The intended use is a stack of contributions from lowest to highest
// specificity, applied in order:
//
//	merged = empty
//	for c in ancestor_virtual_contributions {  // lowest specificity first
//	    merged = mergeOverlay(merged, c)
//	}
//	merged = mergeOverlay(merged, on_disk_at_this_level)
//
// Each successive overlay overrides what came before for the same key.
func mergeOverlay(base, top ZddcFile) ZddcFile {
	out := base

	if top.Title != "" {
		out.Title = top.Title
	}
	if top.CreatedBy != "" {
		out.CreatedBy = top.CreatedBy
	}
	if top.Inherit != nil {
		out.Inherit = top.Inherit
	}
	if top.DefaultTool != "" {
		out.DefaultTool = top.DefaultTool
	}
	if top.DirTool != "" {
		out.DirTool = top.DirTool
	}
	if top.AutoOwn != nil {
		out.AutoOwn = top.AutoOwn
	}
	if top.AutoOwnFenced != nil {
		out.AutoOwnFenced = top.AutoOwnFenced
	}
	// AutoOwnRoles: presence (non-nil) overrides; a deeper level
	// declaring an empty list replaces (and explicitly suppresses)
	// the ancestor's role list. This matches the leaf-wins semantic
	// for the other path-tree contribution lists.
	if top.AutoOwnRoles != nil {
		out.AutoOwnRoles = top.AutoOwnRoles
	}
	if top.DropTarget != nil {
		out.DropTarget = top.DropTarget
	}
	if top.PartySource != "" {
		out.PartySource = top.PartySource
	}
	if top.History != nil {
		out.History = top.History
	}
	if len(top.HistoryGlobs) > 0 {
		out.HistoryGlobs = top.HistoryGlobs
	}
	// Worm: presence (non-nil, even empty) marks the WORM zone.
	// Concat-dedupe across levels (a deeper .zddc adds controllers);
	// preserve a non-nil empty slice so `worm: []` survives the
	// overlay.
	if top.Worm != nil {
		out.Worm = mergeStringSlicePreserveEmpty(out.Worm, top.Worm)
	}
	if top.Virtual != nil {
		out.Virtual = top.Virtual
	}
	if top.ReceivedPath != "" {
		out.ReceivedPath = top.ReceivedPath
	}
	if top.PlannedReviewDate != "" {
		out.PlannedReviewDate = top.PlannedReviewDate
	}
	if top.PlannedResponseDate != "" {
		out.PlannedResponseDate = top.PlannedResponseDate
	}
	out.AvailableTools = mergeStringSlice(out.AvailableTools, top.AvailableTools)

	out.Admins = mergeStringSlice(out.Admins, top.Admins)
	if top.ACL.Inherit != nil {
		out.ACL.Inherit = top.ACL.Inherit
	}

	out.ACL.Permissions = mergeStringMap(out.ACL.Permissions, top.ACL.Permissions)
	out.Tables = mergeStringMap(out.Tables, top.Tables)
	out.Display = mergeStringMap(out.Display, top.Display)

	// Views: per-shape latest-wins (a deeper level overrides a shape, others
	// inherit). Mirror of the Roles/Records map merge.
	if len(top.Views) > 0 {
		merged := make(map[string]ViewSpec, len(out.Views)+len(top.Views))
		for k, v := range out.Views {
			merged[k] = v
		}
		for k, v := range top.Views {
			merged[k] = v
		}
		out.Views = merged
	}

	// Convert: per-key latest-wins. Pointer-to-struct so we can tell
	// "absent" from "explicitly empty" — the latter is rare but valid
	// (an operator who wants to suppress a deployment-default value).
	// Empty top values do NOT clear the ancestor value; operators must
	// set an explicit non-empty string to override.
	if top.Convert != nil {
		if out.Convert == nil {
			out.Convert = &ConvertMetadata{}
		}
		if top.Convert.Client != "" {
			out.Convert.Client = top.Convert.Client
		}
		if top.Convert.Project != "" {
			out.Convert.Project = top.Convert.Project
		}
		if top.Convert.Contractor != "" {
			out.Convert.Contractor = top.Convert.Contractor
		}
		if top.Convert.ProjectNumber != "" {
			out.Convert.ProjectNumber = top.Convert.ProjectNumber
		}
	}

	// Roles: per-name merge (top wins on name clash). This combines
	// the on-disk .zddc at a level with any virtual contributions
	// from ancestor paths: at the same level. Cross-LEVEL role
	// membership union (and the reset flag) is handled at lookup
	// time by lookupRoleMembers, not here.
	if len(top.Roles) > 0 {
		if out.Roles == nil {
			out.Roles = make(map[string]Role, len(top.Roles))
		} else {
			merged := make(map[string]Role, len(out.Roles)+len(top.Roles))
			for k, v := range out.Roles {
				merged[k] = v
			}
			out.Roles = merged
		}
		for k, v := range top.Roles {
			out.Roles[k] = v
		}
	}

	// FieldCodes: map-merge keyed by field name. Top wins on key
	// clash — a sub-tree can narrow or replace a single code's
	// vocabulary without dropping unrelated codes. Mirror of Apps.
	if len(top.FieldCodes) > 0 {
		if out.FieldCodes == nil {
			out.FieldCodes = make(map[string]FieldCode, len(top.FieldCodes))
		} else {
			merged := make(map[string]FieldCode, len(out.FieldCodes)+len(top.FieldCodes))
			for k, v := range out.FieldCodes {
				merged[k] = v
			}
			out.FieldCodes = merged
		}
		for k, v := range top.FieldCodes {
			out.FieldCodes[k] = v
		}
	}

	// Records: map-merge keyed by filename pattern. Each entry's
	// inner fields merge via mergeRecordRule (scalars overwrite,
	// FieldDefaults map-merge, Locked concat-dedupe). Two different
	// patterns at different cascade levels coexist as independent
	// entries; identical patterns merge their contents.
	if len(top.Records) > 0 {
		if out.Records == nil {
			out.Records = make(map[string]RecordRule, len(top.Records))
		} else {
			merged := make(map[string]RecordRule, len(out.Records)+len(top.Records))
			for k, v := range out.Records {
				merged[k] = v
			}
			out.Records = merged
		}
		for k, v := range top.Records {
			out.Records[k] = mergeRecordRule(out.Records[k], v)
		}
	}

	// Paths: top entirely replaces base if set. Recursive descent of
	// the walker is what threads ancestor Paths through to the right
	// level — merging Paths maps themselves at this layer would
	// double-apply.
	if len(top.Paths) > 0 {
		out.Paths = top.Paths
	}

	return out
}

func mergeStringMap(base, top map[string]string) map[string]string {
	if len(top) == 0 {
		return base
	}
	if len(base) == 0 {
		out := make(map[string]string, len(top))
		for k, v := range top {
			out[k] = v
		}
		return out
	}
	out := make(map[string]string, len(base)+len(top))
	for k, v := range base {
		out[k] = v
	}
	for k, v := range top {
		out[k] = v
	}
	return out
}

// mergeStringSlicePreserveEmpty is mergeStringSlice but always returns
// a non-nil result when top is non-nil — so an empty `worm: []` in a
// .zddc still marks the WORM zone after the overlay. Caller is
// expected to only invoke this when top != nil.
func mergeStringSlicePreserveEmpty(base, top []string) []string {
	seen := make(map[string]struct{}, len(base)+len(top))
	out := make([]string, 0, len(base)+len(top))
	for _, v := range base {
		if _, ok := seen[v]; ok {
			continue
		}
		seen[v] = struct{}{}
		out = append(out, v)
	}
	for _, v := range top {
		if _, ok := seen[v]; ok {
			continue
		}
		seen[v] = struct{}{}
		out = append(out, v)
	}
	return out
}

func mergeStringSlice(base, top []string) []string {
	if len(top) == 0 {
		return base
	}
	if len(base) == 0 {
		out := make([]string, len(top))
		copy(out, top)
		return out
	}
	// Concatenate with dedupe (preserve order: base first, then
	// top entries that weren't already in base).
	seen := make(map[string]struct{}, len(base)+len(top))
	out := make([]string, 0, len(base)+len(top))
	for _, v := range base {
		if _, ok := seen[v]; ok {
			continue
		}
		seen[v] = struct{}{}
		out = append(out, v)
	}
	for _, v := range top {
		if _, ok := seen[v]; ok {
			continue
		}
		seen[v] = struct{}{}
		out = append(out, v)
	}
	return out
}
