package datastore

import (
	"context"
	"fmt"
	"strings"

	"github.com/mem9-ai/drive9/pkg/pathutil"
)

// FSLayerRenameHop is one dest hop in a folded rename chain (oldest first).
type FSLayerRenameHop struct {
	From     string
	To       string
	LayerID  string
	EntrySeq int64
}

// FSLayerOverlayNode is the final overlay state of one path after replaying
// the chain log in entry_seq order (D19). Chmod/rename are folded in.
type FSLayerOverlayNode struct {
	Path             string
	Whiteout         bool
	ConsumedByRename bool   // src whiteout already represented by dest rename draft
	RenameFrom       string // original main source (folded)
	RenameVia        string // immediate source of this dest hop
	RenameHops       []FSLayerRenameHop
	Kind             FSLayerEntryKind
	Mode             uint32
	ModeSet          bool
	HasBody          bool
	Entry            FSLayerEntry
}

// MaterializeFSLayerOverlay replays the chain effective log into a path→node map.
// tipMaxSeq nil means tip is unbounded; non-nil limits only the tip layer.
func (s *Store) MaterializeFSLayerOverlay(ctx context.Context, layerID string, tipMaxSeq *int64) (map[string]FSLayerOverlayNode, error) {
	log, err := s.listFSLayerChainEffectiveLog(ctx, layerID, tipMaxSeq)
	if err != nil {
		return nil, err
	}
	return replayFSLayerOverlay(log), nil
}

func replayFSLayerOverlay(log []FSLayerEntry) map[string]FSLayerOverlayNode {
	tree := make(map[string]FSLayerOverlayNode, len(log))
	for i := range log {
		applyFSLayerOverlayOp(tree, &log[i])
	}
	return tree
}

func applyFSLayerOverlayOp(tree map[string]FSLayerOverlayNode, op *FSLayerEntry) {
	if op == nil || op.Path == "" {
		return
	}
	switch op.Op {
	case FSLayerEntryOpWhiteout:
		if prev, ok := tree[op.Path]; ok {
			unconsumeOverlayRenameSource(tree, prev.RenameFrom)
		}
		node := overlayFromEntry(op)
		node.Whiteout = true
		node.HasBody = false
		node.RenameFrom = ""
		// Dir whiteout hides overlay descendants.
		if op.Kind == FSLayerEntryKindDir || strings.HasSuffix(op.Path, "/") {
			clearOverlayDescendants(tree, op.Path)
		}
		tree[op.Path] = node
	case FSLayerEntryOpRename:
		target := overlayRenameTarget(op)
		if target == "" {
			return
		}
		if target == op.Path || target == strings.TrimSuffix(op.Path, "/") || op.Path == strings.TrimSuffix(target, "/") {
			return
		}
		if OverlayRenameDestInsideSrc(op.Path, target) {
			// POSIX rename into own subtree is EINVAL; skip rather than
			// whiteout the whole source tree.
			return
		}
		src, ok := OverlayLookup(tree, op.Path)
		if (ok && src.Whiteout) || OverlayViewHidden(tree, op.Path) {
			// Rename of a deleted path (exact or ancestor-hidden) must not
			// clear dest descendants or move hidden source descendants.
			return
		}
		if overlayRenameMovesDir(op, src, ok, tree) {
			clearOverlayDescendants(tree, target)
			moveOverlayDescendants(tree, op.Path, target)
			src, ok = tree[op.Path]
		}
		if ok && !src.Whiteout && src.HasBody {
			if prev, destOK := tree[target]; destOK {
				unconsumeOverlayRenameSource(tree, prev.RenameFrom)
			}
			moved := src
			moved.Path = target
			moved.Entry.Path = target
			// Dest is a new main path. Keep OCC on the source whiteout.
			moved.Entry.BaseInodeID = ""
			moved.Entry.BaseRevision = 0
			moved.RenameFrom = ""
			moved.RenameVia = ""
			moved.RenameHops = nil
			tree[target] = moved
			wo := overlayFromEntry(op)
			wo.Path = op.Path
			wo.Whiteout = true
			wo.HasBody = false
			wo.RenameFrom = ""
			wo.Entry.Op = FSLayerEntryOpWhiteout
			wo.Entry.Path = op.Path
			wo.Entry.BaseInodeID = src.Entry.BaseInodeID
			wo.Entry.BaseRevision = src.Entry.BaseRevision
			// Overlay-local dest body (mv a b; write b) is claim-less. Keep
			// the rename op's write-time stamp so a later hop still OCCs
			// against live main at this path.
			if wo.Entry.BaseInodeID == "" && wo.Entry.BaseRevision == 0 {
				wo.Entry.BaseInodeID = op.BaseInodeID
				wo.Entry.BaseRevision = op.BaseRevision
			}
			tree[op.Path] = wo
			return
		}
		// Main-backed rename: keep a real rename draft on dest, mark src consumed.
		// RenameFrom is the original main source (fold). RenameVia is this hop.
		via := op.Path
		renameFrom := via
		var hops []FSLayerRenameHop
		if ok && !src.Whiteout {
			if src.RenameFrom != "" {
				renameFrom = src.RenameFrom
			}
			if len(src.RenameHops) > 0 {
				hops = append([]FSLayerRenameHop(nil), src.RenameHops...)
			}
		}
		hops = append(hops, FSLayerRenameHop{From: via, To: target, LayerID: op.LayerID, EntrySeq: op.EntrySeq})
		if prev, destOK := tree[target]; destOK && prev.RenameFrom != "" && prev.RenameFrom != renameFrom {
			unconsumeOverlayRenameSource(tree, prev.RenameFrom)
		}
		dst := overlayFromEntry(op)
		dst.Path = target
		dst.RenameFrom = renameFrom
		dst.RenameVia = via
		dst.RenameHops = hops
		dst.HasBody = false
		dst.Whiteout = false
		dst.Mode = src.Mode
		dst.ModeSet = src.ModeSet
		dst.Entry.Op = FSLayerEntryOpRename
		dst.Entry.Path = renameFrom
		dst.Entry.ContentText = target
		if src.ModeSet {
			dst.Entry.Mode = src.Mode
		}
		// Folded dest path is the original source (RenameFrom). Always keep
		// that source claim. A later hop's snapshot describes the intermediate
		// dest and stays on the intermediate whiteout.
		if src.RenameFrom != "" {
			dst.Entry.BaseInodeID = src.Entry.BaseInodeID
			dst.Entry.BaseRevision = src.Entry.BaseRevision
		}
		tree[target] = dst
		wo := overlayFromEntry(op)
		wo.Path = op.Path
		wo.Whiteout = true
		// Intermediate hops (mv a b; mv b c) must keep a real whiteout at b.
		// Only the original source is implied by the dest rename draft.
		wo.ConsumedByRename = renameFrom == op.Path
		wo.HasBody = false
		wo.RenameFrom = ""
		wo.Entry.Op = FSLayerEntryOpWhiteout
		wo.Entry.Path = op.Path
		tree[op.Path] = wo
		if renameFrom != op.Path {
			if orig, origOK := tree[renameFrom]; origOK && (orig.Whiteout || orig.ConsumedByRename) && orig.RenameFrom == "" {
				// Consume a leftover source whiteout only. Do not clobber a
				// dest draft that now lives at that path (swap: a→tmp, b→a, tmp→b).
				orig.ConsumedByRename = true
				orig.Whiteout = true
				orig.HasBody = false
				tree[renameFrom] = orig
			}
		}
	case FSLayerEntryOpChmod:
		cur, ok := tree[op.Path]
		if ok && cur.Whiteout {
			// Chmod on a deleted path keeps the hide; only fold mode metadata.
			cur.Mode = op.Mode
			cur.ModeSet = true
			cur.Entry.Mode = op.Mode
			tree[op.Path] = cur
			return
		}
		if !ok {
			node := overlayFromEntry(op)
			node.ModeSet = true
			node.HasBody = false
			tree[op.Path] = node
			return
		}
		cur.Mode = op.Mode
		cur.ModeSet = true
		cur.Entry.Mode = op.Mode
		tree[op.Path] = cur
	default:
		prev, hadPrev := tree[op.Path]
		node := overlayFromEntry(op)
		if op.Op == FSLayerEntryOpUpsert || op.Op == FSLayerEntryOpSymlink || op.Op == FSLayerEntryOpMkdir {
			node.HasBody = true
			node.Whiteout = false
			node.RenameFrom = ""
			if hadPrev {
				unconsumeOverlayRenameSource(tree, prev.RenameFrom)
			}
		}
		tree[op.Path] = node
	}
}

func unconsumeOverlayRenameSource(tree map[string]FSLayerOverlayNode, src string) {
	if src == "" {
		return
	}
	n, ok := tree[src]
	if !ok || !n.ConsumedByRename {
		return
	}
	n.ConsumedByRename = false
	tree[src] = n
}

func clearOverlayDescendants(tree map[string]FSLayerOverlayNode, dir string) {
	prefix := dir
	if !strings.HasSuffix(prefix, "/") {
		prefix += "/"
	}
	var toDelete []string
	var sources []string
	for p, n := range tree {
		if strings.HasPrefix(p, prefix) {
			toDelete = append(toDelete, p)
			if n.RenameFrom != "" {
				sources = append(sources, n.RenameFrom)
			}
		}
	}
	for _, src := range sources {
		unconsumeOverlayRenameSource(tree, src)
	}
	for _, p := range toDelete {
		delete(tree, p)
	}
}

func overlayFromEntry(op *FSLayerEntry) FSLayerOverlayNode {
	return FSLayerOverlayNode{
		Path: op.Path,
		Kind: op.Kind,
		Mode: op.Mode,
		// ModeSet is only flipped by chmod so default stored modes (0644)
		// on rename/upsert do not emit a follow-up chmod.
		ModeSet:  false,
		HasBody:  op.Op == FSLayerEntryOpUpsert || op.Op == FSLayerEntryOpSymlink || op.Op == FSLayerEntryOpMkdir,
		Entry:    *op,
		Whiteout: op.Op == FSLayerEntryOpWhiteout,
	}
}

func overlayRenameTarget(op *FSLayerEntry) string {
	if op == nil {
		return ""
	}
	target := op.ContentText
	if target == "" && len(op.ContentBlob) > 0 {
		target = string(op.ContentBlob)
	}
	if target == "" {
		return ""
	}
	isDir := op.Kind == FSLayerEntryKindDir || strings.HasSuffix(op.Path, "/") || strings.HasSuffix(target, "/")
	var (
		can string
		err error
	)
	if isDir {
		can, err = pathutil.CanonicalizeDir(target)
	} else {
		can, err = pathutil.Canonicalize(target)
	}
	if err != nil {
		return target
	}
	return can
}

func overlayDirPrefix(p string) string {
	if strings.HasSuffix(p, "/") {
		return p
	}
	return p + "/"
}

// OverlayRenameDestInsideSrc reports whether dest is a strict descendant of src.
func OverlayRenameDestInsideSrc(src, dest string) bool {
	srcPrefix := overlayDirPrefix(src)
	destPrefix := overlayDirPrefix(dest)
	return destPrefix != srcPrefix && strings.HasPrefix(destPrefix, srcPrefix)
}

func overlayRenameMovesDir(op *FSLayerEntry, src FSLayerOverlayNode, srcOK bool, tree map[string]FSLayerOverlayNode) bool {
	if op.Kind == FSLayerEntryKindDir || strings.HasSuffix(op.Path, "/") {
		return true
	}
	if srcOK && (src.Kind == FSLayerEntryKindDir || strings.HasSuffix(src.Path, "/")) {
		return true
	}
	prefix := overlayDirPrefix(op.Path)
	for p := range tree {
		if strings.HasPrefix(p, prefix) {
			return true
		}
	}
	return false
}

func moveOverlayDescendants(tree map[string]FSLayerOverlayNode, src, dest string) {
	srcPrefix := overlayDirPrefix(src)
	destPrefix := overlayDirPrefix(dest)
	if srcPrefix == destPrefix {
		return
	}
	type item struct {
		path string
		node FSLayerOverlayNode
	}
	var moved []item
	for p, n := range tree {
		if p == src || p == strings.TrimSuffix(src, "/") {
			continue
		}
		if strings.HasPrefix(p, srcPrefix) {
			moved = append(moved, item{path: p, node: n})
		}
	}
	for _, it := range moved {
		delete(tree, it.path)
	}
	for _, it := range moved {
		newP := destPrefix + strings.TrimPrefix(it.path, srcPrefix)
		n := it.node
		n.Path = newP
		n.Entry.Path = newP
		oldFrom := n.RenameFrom
		if n.RenameFrom != "" {
			n.RenameFrom = overlayRewritePrefix(n.RenameFrom, src, srcPrefix, dest, destPrefix)
		}
		// Keep copy-up when the rename source stayed outside the moved tree.
		if n.HasBody || n.RenameFrom == "" || n.RenameFrom != oldFrom {
			n.Entry.BaseInodeID = ""
			n.Entry.BaseRevision = 0
		}
		if n.RenameVia != "" {
			n.RenameVia = overlayRewritePrefix(n.RenameVia, src, srcPrefix, dest, destPrefix)
		}
		if len(n.RenameHops) > 0 {
			hops := append([]FSLayerRenameHop(nil), n.RenameHops...)
			for i := range hops {
				hops[i].From = overlayRewritePrefix(hops[i].From, src, srcPrefix, dest, destPrefix)
				hops[i].To = overlayRewritePrefix(hops[i].To, src, srcPrefix, dest, destPrefix)
			}
			n.RenameHops = hops
		}
		if n.Entry.Op == FSLayerEntryOpRename && n.Entry.ContentText != "" {
			if n.Entry.ContentText == src || strings.HasPrefix(n.Entry.ContentText, srcPrefix) {
				n.Entry.ContentText = destPrefix + strings.TrimPrefix(n.Entry.ContentText, srcPrefix)
			}
		}
		tree[newP] = n
	}
}

// OverlayNodeEntry returns the folded effective entry for reads (GET/diff/FUSE).
func OverlayNodeEntry(n FSLayerOverlayNode) FSLayerEntry {
	e := n.Entry
	e.Path = n.Path
	if n.Whiteout {
		e.Op = FSLayerEntryOpWhiteout
		e.Path = n.Path
	} else if n.RenameFrom != "" && !n.HasBody {
		e.Op = FSLayerEntryOpRename
		e.Path = n.RenameFrom
		e.ContentText = n.Path
	} else if n.HasBody {
		if e.Op == FSLayerEntryOpChmod || e.Op == FSLayerEntryOpRename {
			e.Op = FSLayerEntryOpUpsert
		}
		e.Path = n.Path
	} else {
		e.Op = FSLayerEntryOpChmod
		e.Path = n.Path
	}
	e.Kind = n.Kind
	if n.ModeSet {
		e.Mode = n.Mode
	}
	return e
}

// OverlayLookup returns the overlay node at path, accepting a trailing-slash
// directory variant. Exact-path only; use OverlayHideOrRemap for ancestors.
func OverlayLookup(tree map[string]FSLayerOverlayNode, path string) (FSLayerOverlayNode, bool) {
	if n, ok := tree[path]; ok {
		return n, true
	}
	if path != "" && !strings.HasSuffix(path, "/") {
		if n, ok := tree[path+"/"]; ok {
			return n, true
		}
	}
	if strings.HasSuffix(path, "/") {
		if n, ok := tree[strings.TrimSuffix(path, "/")]; ok {
			return n, true
		}
	}
	return FSLayerOverlayNode{}, false
}

// OverlayHideOrRemap walks ancestor directories after an exact-path miss.
// A directory whiteout (including a rename-source whiteout) hides the path.
// A directory rename dest remaps the remainder onto RenameFrom for main fallback.
func overlayDirRenameSourceIf(n FSLayerOverlayNode, dir string, landed func(FSLayerRenameHop) bool) string {
	if n.Whiteout {
		return ""
	}
	src := OverlayLiveRenameSourceIf(n, landed)
	if src == "" {
		return ""
	}
	if n.Kind == FSLayerEntryKindDir || strings.HasSuffix(dir, "/") || strings.HasSuffix(src, "/") {
		return src
	}
	return ""
}

func overlayRewritePrefix(p, src, srcPrefix, dest, destPrefix string) string {
	if p == src {
		return dest
	}
	if strings.HasPrefix(p, srcPrefix) {
		return destPrefix + strings.TrimPrefix(p, srcPrefix)
	}
	return p
}

func OverlayViewHidden(tree map[string]FSLayerOverlayNode, path string) bool {
	for _, dir := range overlayAncestorDirs(path) {
		n, ok := OverlayLookup(tree, dir)
		if ok && n.Whiteout {
			return true
		}
	}
	return false
}

func OverlayRemapToMainIf(tree map[string]FSLayerOverlayNode, path string, landed func(FSLayerRenameHop) bool) string {
	return overlayRemapToMain(tree, path, landed)
}

func overlayRemapToMain(tree map[string]FSLayerOverlayNode, path string, landed func(FSLayerRenameHop) bool) string {
	mapped := path
	for hop := 0; hop < FSLayerHardMaxDepth+4; hop++ {
		next := overlayRemapOneHop(tree, mapped, landed)
		if next == mapped {
			return mapped
		}
		mapped = next
	}
	return mapped
}

// overlayRemapOneHop applies the innermost dest-dir remap whose prefix still
// matches the current path. Callers iterate until stable so nested dest-space
// hops (mv old→tmp then tmp/sub→new/sub) invert onto live main, and so an
// outer dest cannot concatenate onto an already-remapped path.
func overlayRemapOneHop(tree map[string]FSLayerOverlayNode, path string, landed func(FSLayerRenameHop) bool) string {
	for _, dir := range overlayAncestorDirs(path) {
		n, ok := OverlayLookup(tree, dir)
		if !ok {
			continue
		}
		src := overlayDirRenameSourceIf(n, dir, landed)
		if src == "" {
			continue
		}
		if OverlayDestHopLanded(n, landed) {
			continue
		}
		prefix := overlayDirPrefix(dir)
		if path != strings.TrimSuffix(dir, "/") && !strings.HasPrefix(path, prefix) {
			continue
		}
		rest := strings.TrimPrefix(path, prefix)
		return overlayDirPrefix(src) + rest
	}
	return path
}

// OverlayLiveRenameSourceIf is the live-main source of a dest draft. It walks
// dest hops oldest-first and keeps the last hop dest whose hop is landed.
func OverlayLiveRenameSourceIf(n FSLayerOverlayNode, landed func(FSLayerRenameHop) bool) string {
	live := n.RenameFrom
	if live == "" {
		live = n.RenameVia
	}
	if landed == nil {
		return live
	}
	for _, hop := range n.RenameHops {
		if hop.LayerID == "" {
			continue
		}
		if landed(hop) {
			live = hop.To
		}
	}
	return live
}

func OverlayRenameHopsAllLanded(n FSLayerOverlayNode, landed func(FSLayerRenameHop) bool) bool {
	if len(n.RenameHops) == 0 || landed == nil {
		return false
	}
	for _, hop := range n.RenameHops {
		if hop.LayerID == "" || !landed(hop) {
			return false
		}
	}
	return true
}

// OverlayDestHopLanded reports whether the dest node's latest hop has already
// landed on live main. Without recorded hops, the node's own entry is the hop.
func OverlayDestHopLanded(n FSLayerOverlayNode, landed func(FSLayerRenameHop) bool) bool {
	if landed == nil {
		return false
	}
	if len(n.RenameHops) > 0 {
		return landed(n.RenameHops[len(n.RenameHops)-1])
	}
	if n.Entry.LayerID == "" {
		return false
	}
	return landed(FSLayerRenameHop{LayerID: n.Entry.LayerID, EntrySeq: n.Entry.EntrySeq})
}

func OverlayHideOrRemap(tree map[string]FSLayerOverlayNode, path string) (hidden bool, mainPath string) {
	return overlayHideOrRemap(tree, path, nil)
}

func OverlayHideOrRemapIf(tree map[string]FSLayerOverlayNode, path string, landed func(FSLayerRenameHop) bool) (hidden bool, mainPath string) {
	return overlayHideOrRemap(tree, path, landed)
}

func overlayHideOrRemap(tree map[string]FSLayerOverlayNode, path string, landed func(FSLayerRenameHop) bool) (hidden bool, mainPath string) {
	if path == "" {
		return false, path
	}
	if OverlayViewHidden(tree, path) {
		return true, ""
	}
	return false, overlayRemapToMain(tree, path, landed)
}

// OverlayMainReadPathIf returns the live-main path for a metadata-only overlay
// node (chmod or main-backed rename). Hide is applied to the *view* path so a
// later whiteout of the original parent does not hide a dest that already
// left that directory. Remap is applied to RenameFrom (or the view path) so
// dest-space pointers still resolve onto uncommitted main.
func OverlayMainReadPathIf(tree map[string]FSLayerOverlayNode, path string, n FSLayerOverlayNode, landed func(FSLayerRenameHop) bool) (hidden bool, mainPath string) {
	return overlayMainReadPath(tree, path, n, landed)
}

func overlayMainReadPath(tree map[string]FSLayerOverlayNode, path string, n FSLayerOverlayNode, landed func(FSLayerRenameHop) bool) (hidden bool, mainPath string) {
	if OverlayViewHidden(tree, path) {
		return true, ""
	}
	lookup := path
	if n.RenameFrom != "" || n.RenameVia != "" {
		lookup = OverlayLiveRenameSourceIf(n, landed)
		if lookup == "" {
			lookup = path
		}
	}
	return false, overlayRemapToMain(tree, lookup, landed)
}

func overlayAncestorDirs(path string) []string {
	var dirs []string
	cur := path
	for {
		parent := pathutil.ParentPath(cur)
		if parent == "" || parent == cur {
			break
		}
		dirs = append(dirs, parent)
		if parent == "/" {
			break
		}
		cur = parent
	}
	return dirs
}

// OverlayCommitDrafts converts overlay nodes to apply-shaped entries.
// Creates/renames come first (shallow paths), then whiteouts (deep paths first)
// so directory deletes see an empty tree (D19 / V1 apply order).
//
// Same-path drafts keep copy-up BaseInodeID/BaseRevision for V1 commit OCC.
// D18 skip-if-equal drops paths that already match live main; it does not
// mean restamping claims from live main (that is last-write-wins).
func OverlayCommitDrafts(tree map[string]FSLayerOverlayNode) []FSLayerEntry {
	var creates, deletes []FSLayerEntry
	for p, n := range tree {
		if n.ConsumedByRename {
			continue
		}
		e := OverlayNodeEntry(n)
		e.Path = p
		if n.RenameFrom != "" && !n.HasBody {
			e.Op = FSLayerEntryOpRename
			e.Path = n.RenameFrom
			e.ContentText = n.Path
			creates = append(creates, e)
			if n.ModeSet {
				chmod := e
				chmod.Op = FSLayerEntryOpChmod
				chmod.Path = n.Path
				chmod.ContentText = ""
				chmod.Mode = n.Mode
				creates = append(creates, chmod)
			}
			continue
		}
		if n.Whiteout {
			e.Op = FSLayerEntryOpWhiteout
			e.Path = n.Path
			deletes = append(deletes, e)
			continue
		}
		if n.HasBody {
			if e.Op == FSLayerEntryOpChmod || e.Op == FSLayerEntryOpRename {
				e.Op = FSLayerEntryOpUpsert
			}
			e.Path = n.Path
			creates = append(creates, e)
			continue
		}
		e.Op = FSLayerEntryOpChmod
		e.Path = n.Path
		creates = append(creates, e)
	}
	sortOverlayCommitCreates(creates)
	sortOverlayCommitDeletes(deletes)
	return append(creates, deletes...)
}

// listFSLayerChainEffectiveLog is the shared implementation with optional tip cap.
func (s *Store) listFSLayerChainEffectiveLog(ctx context.Context, layerID string, tipMaxSeq *int64) ([]FSLayerEntry, error) {
	chain, err := s.ListFSLayerChain(ctx, layerID)
	if err != nil {
		return nil, fmt.Errorf("list fs layer chain %s: %w", layerID, err)
	}
	if tipMaxSeq != nil && *tipMaxSeq < 0 {
		return nil, fmt.Errorf("fs layer max seq must be non-negative")
	}
	chain = applyTipLimit(chain, tipMaxSeq)
	var out []FSLayerEntry
	for _, frame := range chain {
		var log []FSLayerEntry
		if frame.LimitSeq < 0 {
			log, err = s.ListFSLayerEntryLog(ctx, frame.Layer.LayerID)
		} else {
			log, err = s.ListFSLayerEntryLogAtSeq(ctx, frame.Layer.LayerID, frame.LimitSeq)
		}
		if err != nil {
			return nil, fmt.Errorf("list fs layer entry log %s: %w", frame.Layer.LayerID, err)
		}
		out = append(out, log...)
	}
	return out, nil
}
