// permission_service.go 实装 ACP 工具调用权限审批框架。
//
// 代理通过 session/request_permission 发起工具调用授权请求，relay 为每个请求起
// 独立 goroutine 调用 PermissionHandler.Handle → PermissionService.Decide。Decide
// 分两条路径：
//   - 命中 agent kind 的 tool_allowlist：自动放行（status=auto），立即返回。
//   - 未命中：落库为 pending、通过 WS 推送 permission_request 事件、阻塞等待人工
//     决策（Resolve）/ 超时（默认拒绝）/ 会话终止（CancelSession）。
//
// 阻塞安全：relay.handleAgentRequest 对每个 agent 请求起独立 goroutine，因此在
// Decide 内阻塞不会卡住 reader 主循环；ctx 来自该请求的 relay.Run，会话关闭时取消。
package acp

import (
	"context"
	"encoding/json"
	"log/slog"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/acp/handlers"
	"github.com/yan1h/agent-coding-workflow/internal/audit"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	"github.com/yan1h/agent-coding-workflow/internal/infra/notify"
)

// DefaultPermissionTimeout 是未配置时的人工决策超时；到期默认拒绝。
const DefaultPermissionTimeout = 5 * time.Minute

// permissionTopic 是 HITL 待审权限通知的 topic（inbox + 跨通道路由用）。
const permissionTopic = "acp.permission_pending"

// PermissionNotifier 是 PermissionService 投递"待人工审批"通知所需的窄接口。
// notify.Dispatcher 满足它；用窄接口便于测试断言 Dispatch 调用。
type PermissionNotifier interface {
	Dispatch(ctx context.Context, msg notify.Message) error
}

// PermissionMetrics 是 PermissionService 记录决策计数所需的窄接口。
// metrics.Metrics 满足它；nil 时不记录（部分测试 / metrics 关闭）。
type PermissionMetrics interface {
	RecordPermissionDecision(outcome string)
}

// RelayLocator 让 PermissionService 按 session id 取到 *Relay 以推送 WS 事件。
// Supervisor 满足该接口（GetRelay）。用窄接口避免 permSvc 依赖整个 Supervisor。
type RelayLocator interface {
	GetRelay(sid uuid.UUID) *Relay
}

// ProjectMaintainer 让 PermissionService 判定某用户是否对会话所属 project 有写权限
// （project maintainer：owner / global-admin / project admin|member 角色）。允许任意
// project maintainer 处理待审权限请求，而不仅是会话 owner（autonomy roadmap §11）。
// app 层用 projectAccessAdapter.ResolveByID 实现；nil 时退化为仅 owner/admin。
type ProjectMaintainer interface {
	// CanWriteProject 报告 (callerID, isAdmin) 是否对 projectID 有写权限。
	CanWriteProject(ctx context.Context, callerID uuid.UUID, isAdmin bool, projectID uuid.UUID) (bool, error)
}

// decision 是投递给阻塞中 Decide 的人工/系统决策结果。
type decision struct {
	optionID string
}

// waiter 关联一个挂起的权限请求与其所属会话，便于会话终止时定向清理。
type waiter struct {
	ch        chan decision
	sessionID uuid.UUID
}

// PermissionService 是进程级单例，注入到所有 relay 的 PermissionHandler。
type PermissionService struct {
	repo    Repository
	audit   audit.Recorder
	log     *slog.Logger
	timeout time.Duration

	mu      sync.Mutex
	waiters map[uuid.UUID]*waiter // requestID -> waiter

	locMu sync.RWMutex
	loc   RelayLocator

	// HITL：待审权限通知 + 决策计数。装配期经 SetNotifier/SetMetrics 注入，
	// 二者均可为 nil（部分测试 / metrics 关闭）。
	notify  PermissionNotifier
	metrics PermissionMetrics

	// maintainer 允许任意 project maintainer（非仅会话 owner）处理待审权限请求。
	// 装配期经 SetProjectMaintainer 注入；nil 时退化为仅 owner/global-admin。
	maintainer ProjectMaintainer
}

// SetNotifier 注入待审权限通知投递器（装配期，best-effort）。
func (s *PermissionService) SetNotifier(n PermissionNotifier) { s.notify = n }

// SetMetrics 注入权限决策计数器（装配期）。
func (s *PermissionService) SetMetrics(m PermissionMetrics) { s.metrics = m }

// SetProjectMaintainer 注入 project maintainer 判定器（装配期）。注入后，会话
// 所属 project 的任意 maintainer 均可处理该会话的待审权限请求。
func (s *PermissionService) SetProjectMaintainer(m ProjectMaintainer) { s.maintainer = m }

// NewPermissionService 构造 PermissionService。timeout<=0 时用 DefaultPermissionTimeout。
func NewPermissionService(repo Repository, rec audit.Recorder, timeout time.Duration, log *slog.Logger) *PermissionService {
	if log == nil {
		log = slog.Default()
	}
	if timeout <= 0 {
		timeout = DefaultPermissionTimeout
	}
	return &PermissionService{
		repo:    repo,
		audit:   rec,
		log:     log,
		timeout: timeout,
		waiters: map[uuid.UUID]*waiter{},
	}
}

// SetRelayLocator 注入 relay 定位器。因 Supervisor 与 PermissionService 互相依赖，
// 装配时先构造二者再回填（与 workspace CloneRunner.SetScheduler 同模式）。
func (s *PermissionService) SetRelayLocator(loc RelayLocator) {
	s.locMu.Lock()
	s.loc = loc
	s.locMu.Unlock()
}

// Decide 实现 handlers.PermissionDecider。返回选中的 option id；空串表示拒绝。
func (s *PermissionService) Decide(ctx context.Context, sess handlers.SessionContext, agentReqID string,
	toolCall json.RawMessage, options []handlers.PermOption) string {

	toolName := extractToolName(toolCall)
	optionsJSON, _ := json.Marshal(options)

	// 路径一：白名单命中 → 自动放行。
	if allowlistHit(sess.ToolAllowlist, toolName) {
		chosen := chooseAllowOption(options)
		_, err := s.repo.InsertPermissionRequest(ctx, &PermissionRequest{
			ID:             uuid.New(),
			SessionID:      sess.SessionID,
			AgentRequestID: agentReqID,
			ToolName:       toolName,
			ToolCall:       jsonOrNull(toolCall),
			Options:        optionsJSON,
			Status:         PermissionAuto,
			ChosenOptionID: strPtrOrNil(chosen),
		})
		if err != nil {
			s.log.Error("acp.permission.insert_auto", "session_id", sess.SessionID, "err", err.Error())
		}
		s.recordDecision(ctx, sess.UserID, sess.SessionID, toolName, "auto", nil)
		s.recordMetric("auto")
		return chosen
	}

	// 路径二：落库 pending 并阻塞等待决策。
	reqID := uuid.New()
	row, err := s.repo.InsertPermissionRequest(ctx, &PermissionRequest{
		ID:             reqID,
		SessionID:      sess.SessionID,
		AgentRequestID: agentReqID,
		ToolName:       toolName,
		ToolCall:       jsonOrNull(toolCall),
		Options:        optionsJSON,
		Status:         PermissionPending,
	})
	if err != nil {
		// 落库失败时无法人工审批，保守默认拒绝。
		s.log.Error("acp.permission.insert_pending", "session_id", sess.SessionID, "err", err.Error())
		return ""
	}

	w := &waiter{ch: make(chan decision, 1), sessionID: sess.SessionID}
	s.mu.Lock()
	s.waiters[reqID] = w
	s.mu.Unlock()
	defer s.removeWaiter(reqID)

	s.fanout(sess.SessionID, &EventEnvelope{
		Kind:    "permission_request",
		Payload: mustJSON(permissionRequestPayload(row)),
	})

	// HITL：在阻塞前 best-effort 推送 inbox 通知，使关闭页面的用户也能被提醒去
	// /approvals 审批（fire-and-forget，错误不影响 deny-on-timeout 安全默认）。
	s.notifyPending(ctx, sess, row, toolName)

	select {
	case d := <-w.ch:
		return d.optionID
	case <-time.After(s.timeout):
		// 超时 → CAS 标记 expired（系统决策，decided_by=NULL）。
		if _, ok, _ := s.repo.DecidePermissionRequest(context.WithoutCancel(ctx), reqID, PermissionExpired, nil, nil); ok {
			s.fanout(sess.SessionID, resolvedEnvelope(reqID, PermissionExpired, ""))
			s.recordMetric("expired")
		}
		return ""
	case <-ctx.Done():
		// relay/进程关闭：CancelSession 已处理 DB；这里仅退出，agent 收默认拒绝。
		return ""
	}
}

// Resolve 落地一次人工决策（approve/deny）。鉴权：session owner、global-admin，或
// 会话所属 project 的任意 maintainer（autonomy roadmap §11）。
func (s *PermissionService) Resolve(ctx context.Context, c Caller, reqID uuid.UUID, approve bool, optionID string) error {
	req, err := s.repo.GetPermissionRequestByID(ctx, reqID)
	if err != nil {
		return err
	}
	sessRow, err := s.repo.GetSessionByID(ctx, req.SessionID)
	if err != nil {
		return err
	}
	if !s.canManageSession(ctx, c, sessRow) {
		return errs.New(errs.CodeAcpSessionNotOwned, "无权处理该会话的权限请求")
	}

	var options []handlers.PermOption
	_ = json.Unmarshal(req.Options, &options)

	var chosen string
	var status PermissionStatus
	if approve {
		chosen = optionID
		if chosen == "" {
			chosen = chooseAllowOption(options)
		} else if !optionExists(options, chosen) {
			return errs.New(errs.CodeAcpPermissionInvalidOption, "无效的选项 id")
		}
		status = PermissionApproved
	} else {
		chosen = chooseRejectOption(options)
		status = PermissionDenied
	}

	decidedBy := c.UserID
	_, ok, err := s.repo.DecidePermissionRequest(ctx, reqID, status, strPtrOrNil(chosen), &decidedBy)
	if err != nil {
		return err
	}
	if !ok {
		// 已被超时/会话终止/重复决策抢先落地。
		return errs.New(errs.CodeAcpPermissionAlreadyDecided, "该权限请求已被处理")
	}

	s.wake(reqID, decision{optionID: chosen})
	s.fanout(req.SessionID, resolvedEnvelope(reqID, status, chosen))
	s.recordDecision(ctx, c.UserID, req.SessionID, req.ToolName, string(status), &c.UserID)
	if approve {
		s.recordMetric("approved")
	} else {
		s.recordMetric("denied")
	}
	return nil
}

// ListPendingForUser 返回该用户跨所有会话的待审权限请求（HITL 跨会话审批 inbox）。
// 包装 repo.ListPendingPermissionRequestsByUser（已 JOIN acp_sessions.user_id）。
// admin 仅看自己拥有会话的待审项（与现有 inbox 语义一致；全局视图走 dashboard）。
func (s *PermissionService) ListPendingForUser(ctx context.Context, c Caller) ([]*PermissionRequest, error) {
	return s.repo.ListPendingPermissionRequestsByUser(ctx, c.UserID)
}

// ListPendingBySession 返回某会话的待审请求。鉴权同 Resolve。
func (s *PermissionService) ListPendingBySession(ctx context.Context, c Caller, sessionID uuid.UUID) ([]*PermissionRequest, error) {
	sessRow, err := s.repo.GetSessionByID(ctx, sessionID)
	if err != nil {
		return nil, err
	}
	if !s.canManageSession(ctx, c, sessRow) {
		return nil, errs.New(errs.CodeAcpSessionNotOwned, "无权查看该会话")
	}
	return s.repo.ListPendingPermissionRequestsBySession(ctx, sessionID)
}

// canManageSession 报告 caller 是否可处理/查看该会话的权限请求：session owner、
// global-admin，或会话所属 project 的任意 maintainer（注入 ProjectMaintainer 时）。
// maintainer 查询失败按"无权"处理（fail-closed），但 owner/admin 短路不受影响。
func (s *PermissionService) canManageSession(ctx context.Context, c Caller, sess *Session) bool {
	if c.IsAdmin || sess.UserID == c.UserID {
		return true
	}
	if s.maintainer == nil {
		return false
	}
	ok, err := s.maintainer.CanWriteProject(ctx, c.UserID, c.IsAdmin, sess.ProjectID)
	if err != nil {
		return false
	}
	return ok
}

// CancelSession 在会话终止时把其全部 pending 请求标 expired，并唤醒阻塞的 Decide
// goroutine 使其立即返回（agent 收默认拒绝）。supervisor.onExit 调用。
func (s *PermissionService) CancelSession(ctx context.Context, sessionID uuid.UUID) {
	if _, err := s.repo.ExpirePendingPermissionRequestsBySession(ctx, sessionID); err != nil {
		s.log.Error("acp.permission.cancel_session", "session_id", sessionID, "err", err.Error())
	}
	s.mu.Lock()
	for reqID, w := range s.waiters {
		if w.sessionID == sessionID {
			select {
			case w.ch <- decision{optionID: ""}:
			default:
			}
			delete(s.waiters, reqID)
		}
	}
	s.mu.Unlock()
}

// ExpireStale 把超过 before 仍 pending 的请求标 expired（启动 reaper / 兜底）。
func (s *PermissionService) ExpireStale(ctx context.Context, before time.Time) (int64, error) {
	return s.repo.ExpireStalePermissionRequests(ctx, before)
}

// ===== 内部辅助 =====

func (s *PermissionService) removeWaiter(reqID uuid.UUID) {
	s.mu.Lock()
	delete(s.waiters, reqID)
	s.mu.Unlock()
}

func (s *PermissionService) wake(reqID uuid.UUID, d decision) {
	s.mu.Lock()
	w, ok := s.waiters[reqID]
	if ok {
		delete(s.waiters, reqID)
	}
	s.mu.Unlock()
	if ok {
		select {
		case w.ch <- d:
		default:
		}
	}
}

func (s *PermissionService) fanout(sessionID uuid.UUID, env *EventEnvelope) {
	s.locMu.RLock()
	loc := s.loc
	s.locMu.RUnlock()
	if loc == nil {
		return
	}
	if relay := loc.GetRelay(sessionID); relay != nil {
		relay.FanoutControl(env)
	}
}

func (s *PermissionService) recordDecision(ctx context.Context, actor, sessionID uuid.UUID, toolName, outcome string, decidedBy *uuid.UUID) {
	if s.audit == nil {
		return
	}
	a := actor
	_ = s.audit.Record(context.WithoutCancel(ctx), audit.Entry{
		UserID:     &a,
		Action:     "acp.permission." + outcome,
		TargetType: "acp_session",
		TargetID:   sessionID.String(),
		Metadata: map[string]any{
			"tool_name":  toolName,
			"decided_by": decidedBy,
		},
	})
}

// recordMetric 记录一次权限决策计数（auto/approved/denied/expired）。metrics 为 nil 时 no-op。
func (s *PermissionService) recordMetric(outcome string) {
	if s.metrics == nil {
		return
	}
	s.metrics.RecordPermissionDecision(outcome)
}

// notifyPending best-effort 向会话所有者推送"待人工审批"通知。错误仅记日志，
// 绝不影响 Decide 的阻塞 / deny-on-timeout 安全默认。
func (s *PermissionService) notifyPending(ctx context.Context, sess handlers.SessionContext, row *PermissionRequest, toolName string) {
	if s.notify == nil {
		return
	}
	if err := s.notify.Dispatch(context.WithoutCancel(ctx), notify.Message{
		UserID:   sess.UserID,
		Topic:    permissionTopic,
		Severity: notify.SeverityWarning,
		Title:    "Approval needed",
		Body:     "An agent is requesting permission to run a tool and needs your approval.",
		Link:     "/approvals",
		Metadata: map[string]any{
			"request_id": row.ID.String(),
			"session_id": sess.SessionID.String(),
			"tool_name":  toolName,
		},
	}); err != nil {
		s.log.Warn("acp.permission.notify_pending_failed", "session_id", sess.SessionID, "err", err.Error())
	}
}

func permissionRequestPayload(p *PermissionRequest) map[string]any {
	return map[string]any{
		"request_id": p.ID.String(),
		"session_id": p.SessionID.String(),
		"tool_name":  p.ToolName,
		"tool_call":  json.RawMessage(p.ToolCall),
		"options":    json.RawMessage(p.Options),
		"status":     string(p.Status),
		"created_at": p.CreatedAt,
	}
}

func resolvedEnvelope(reqID uuid.UUID, status PermissionStatus, chosen string) *EventEnvelope {
	return &EventEnvelope{
		Kind: "permission_resolved",
		Payload: mustJSON(map[string]any{
			"request_id":       reqID.String(),
			"status":           string(status),
			"chosen_option_id": chosen,
		}),
	}
}

// extractToolName 尝试从 toolCall JSON 中提取真实工具标识。
//
// 安全约束（自动放行依据）：只接受真实工具名字段（name / toolName）。绝不回退到
// ACP toolCall 的 'kind'（粗粒度枚举 read/edit/execute…）或 'title'（自由文本）——
// 二者都由不可信 agent 自行填写，agent 可把一个 execute/shell 调用标成 kind='read'
// 或冒用白名单里的 title 来绕过人工审批闸门。取不到真实名时返回空串，使 allowlistHit
// 一律不放行（保守走人工审批），同时保留空名默认拒绝的语义。
func extractToolName(toolCall json.RawMessage) string {
	if len(toolCall) == 0 {
		return ""
	}
	var m map[string]any
	if err := json.Unmarshal(toolCall, &m); err != nil {
		return ""
	}
	for _, k := range []string{"name", "toolName"} {
		if v, ok := m[k].(string); ok && v != "" {
			return v
		}
	}
	return ""
}

// allowlistHit 判断工具是否在白名单内。含 "*" 表示放行全部可识别的工具；
// toolName 为空（无法识别的 tool call）时一律不放行，保守走人工审批。
func allowlistHit(allowlist []string, toolName string) bool {
	if toolName == "" {
		return false
	}
	for _, a := range allowlist {
		if a == "*" || a == toolName {
			return true
		}
	}
	return false
}

// chooseAllowOption 选一个“批准”类选项 id：优先包含 allow/yes 的，否则取首个。
func chooseAllowOption(options []handlers.PermOption) string {
	if id := matchOption(options, []string{"allow", "yes", "approve", "accept"}); id != "" {
		return id
	}
	if len(options) > 0 {
		return options[0].ID
	}
	return ""
}

// chooseRejectOption 选一个“拒绝”类选项 id：优先包含 reject/deny/no 的，否则空串。
func chooseRejectOption(options []handlers.PermOption) string {
	return matchOption(options, []string{"reject", "deny", "no"})
}

func matchOption(options []handlers.PermOption, keywords []string) string {
	for _, opt := range options {
		lc := strings.ToLower(opt.ID)
		for _, kw := range keywords {
			if strings.Contains(lc, kw) {
				return opt.ID
			}
		}
	}
	return ""
}

func optionExists(options []handlers.PermOption, id string) bool {
	for _, opt := range options {
		if opt.ID == id {
			return true
		}
	}
	return false
}

func strPtrOrNil(s string) *string {
	if s == "" {
		return nil
	}
	return &s
}

func jsonOrNull(b json.RawMessage) []byte {
	if len(b) == 0 {
		return []byte("null")
	}
	return b
}
