// Package pathsafe 实现 ACP fs/* method 的路径安全校验。
//
// 单独成包是因为 internal/acp/handlers 子包不能 import internal/acp 主包
// （acp 依赖 handlers，会循环），故抽到共享子包供两边复用。
//
// 规则（spec §5.6）：
//  1. 路径必须以 cwd_path 为前缀（防 ../../etc/passwd）
//  2. 解析 symlink 后的真实路径必须仍在 cwd_path 下（防 symlink 逃逸）；
//     目标不存在时（如 fs/write_text_file 新建文件），向上找最近已存在的
//     祖先目录解析真实路径后再校验，防经由指向外部的目录 symlink 写出 worktree。
//
// 三重校验：filepath.Abs + hasPathPrefix + filepath.EvalSymlinks。
package pathsafe

import (
	"path/filepath"
	"strings"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)

// ResolveSafePath 校验 requested 是否安全（在 cwd 内）。
// cwd 必须是绝对路径；requested 可以是绝对或相对（相对时基于 cwd 解析）。
// 返回校验通过后的绝对路径（含 symlink 解析）；失败返回 CodeAcpFsPathOutsideWorktree。
func ResolveSafePath(cwd, requested string) (string, error) {
	if !filepath.IsAbs(cwd) {
		return "", errs.New(errs.CodeAcpFsPathOutsideWorktree, "cwd must be absolute")
	}
	cleanCwd := filepath.Clean(cwd)

	var abs string
	if filepath.IsAbs(requested) {
		abs = filepath.Clean(requested)
	} else {
		abs = filepath.Clean(filepath.Join(cleanCwd, requested))
	}

	if !hasPathPrefix(abs, cleanCwd) {
		return "", errs.New(errs.CodeAcpFsPathOutsideWorktree, "path outside worktree")
	}

	real, err := filepath.EvalSymlinks(abs)
	if err != nil {
		// 目标（或部分祖先）不存在：不能静默放行，否则父目录若是指向外部的
		// symlink，后续 MkdirAll/WriteFile 会跟随 symlink 写出 worktree。
		// 改为向上找最近已存在的祖先解析真实路径，再拼回剩余部分校验。
		real, err = evalNearestAncestor(abs)
		if err != nil {
			return "", errs.Wrap(err, errs.CodeAcpFsPathOutsideWorktree, "resolve path")
		}
	}
	if !hasPathPrefix(real, cleanCwd) {
		return "", errs.New(errs.CodeAcpFsPathOutsideWorktree, "path resolves outside worktree via symlink")
	}
	return abs, nil
}

// evalNearestAncestor 自 abs 向上找最近已存在（EvalSymlinks 成功）的祖先目录，
// 用其真实路径拼回剩余相对部分，得到 abs 对应的真实路径。
// 直到根目录仍无法解析时返回错误（调用方按路径不安全处理）。
func evalNearestAncestor(abs string) (string, error) {
	cur := abs
	rest := ""
	for {
		parent := filepath.Dir(cur)
		if parent == cur {
			// 已到根目录仍解析失败（如盘符/根不存在）
			return "", errs.New(errs.CodeAcpFsPathOutsideWorktree, "no existing ancestor for path")
		}
		if rest == "" {
			rest = filepath.Base(cur)
		} else {
			rest = filepath.Join(filepath.Base(cur), rest)
		}
		cur = parent
		if real, err := filepath.EvalSymlinks(cur); err == nil {
			return filepath.Join(real, rest), nil
		}
	}
}

// hasPathPrefix reports whether abs is equal to base or under base. Trailing
// separator handling is critical: we want "/foo/bar" to be inside "/foo" but
// NOT inside "/fo".
func hasPathPrefix(abs, base string) bool {
	if abs == base {
		return true
	}
	sep := string(filepath.Separator)
	return strings.HasPrefix(abs, base+sep)
}
