// relay.go 实现 ACP 双向 JSON-RPC 中继：
//   - reader: 子进程 stdout → 解析 JSON-RPC → 落 events → fanout / handler dispatch
//   - writer: 服务端发起 / WS 客户端透传 → 子进程 stdin
//   - subscribers: 多 WS tab 共享 reader 的 fanout
//
// id 命名空间（spec §5.2 / §7.2）：
//   - 服务端发起：int64（atomic 递增）
//   - 客户端 WS 发起：string（前端自生成）
//   - inflight 仅存 int64 → response chan；string id 的 response 当 event fanout
package acp

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"sync"
	"sync/atomic"

	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/acp/handlers"
)

// EventEnvelope 是推到 WS 的事件结构（与落库的 Event 字段同步，但带 ID 数值化方便 JSON）。
type EventEnvelope struct {
	ID        int64           `json:"id"`
	Direction string          `json:"direction"` // 'in' | 'out'
	Kind      string          `json:"kind"`      // 'request'|'response'|'notification'|'error'|'session_terminated'|'slow_consumer_disconnect'
	Method    string          `json:"method,omitempty"`
	Payload   json.RawMessage `json:"payload"`
	Truncated bool            `json:"truncated"`
}

// Subscriber 是单个 WS tab 的订阅句柄。
type Subscriber struct {
	ID     uuid.UUID
	UserID uuid.UUID
	Send   chan *EventEnvelope
	closed atomic.Bool
}

// Relay 是单 session 的中继器。每个活跃 session 一个 Relay 实例。
type Relay struct {
	sessionID uuid.UUID

	// 协议层
	enc *Encoder
	dec *Decoder

	// 服务端发起请求等响应（id=int64）
	inflight sync.Map // int64 → chan *Message
	nextID   atomic.Int64

	// WS subscribers（多 tab fanout）
	subsMu sync.Mutex
	subs   map[uuid.UUID]*Subscriber

	// 写入路径
	serverInitiated chan *Message // 服务端发起的 request/notification
	clientInitiated chan *Message // WS 上行（已校验 method 白名单）

	// 依赖
	repo        Repository
	fsHandler   *handlers.FsHandler
	permHandler *handlers.PermissionHandler
	log         *slog.Logger
	cfg         RelayConfig

	// 回合完成检测：被动解析 session/update + session/prompt 响应的 stopReason。
	// 由 supervisor.Spawn 通过 SetTracker 注入；可为 nil（部分测试）。
	tracker *TurnTracker

	// 成本核算：被动解析 agent→client 消息中的 token/cost 用量并累加 + 预算判定。
	// 由 supervisor.Spawn 通过 SetUsageSink 注入；可为 nil（部分测试 / 未配置）。
	usageSink usageSink

	// 终止控制
	closed atomic.Bool
	done   chan struct{}
}

// RelayConfig 影响事件入库截断行为。
type RelayConfig struct {
	EventMaxPayload    int
	EventTruncateField int
	WSSendBuffer       int
}

// NewRelay 构造一个空 Relay；启动 goroutine 走 Run（D7 实现）。
func NewRelay(sessionID uuid.UUID, dec *Decoder, enc *Encoder,
	repo Repository, fs *handlers.FsHandler, perm *handlers.PermissionHandler,
	cfg RelayConfig, log *slog.Logger) *Relay {
	if log == nil {
		log = slog.Default()
	}
	return &Relay{
		sessionID:       sessionID,
		enc:             enc,
		dec:             dec,
		subs:            map[uuid.UUID]*Subscriber{},
		serverInitiated: make(chan *Message, 32),
		clientInitiated: make(chan *Message, 32),
		repo:            repo,
		fsHandler:       fs,
		permHandler:     perm,
		log:             log,
		cfg:             cfg,
		done:            make(chan struct{}),
	}
}

// SetTracker 注入回合追踪器。由 supervisor.Spawn 在 NewRelay 之后调用。
func (r *Relay) SetTracker(t *TurnTracker) { r.tracker = t }

// SetUsageSink 注入成本核算 sink。由 supervisor.Spawn 在 NewRelay 之后调用；
// 可为 nil（部分测试 / 未配置 model 价格时由 supervisor 自行决定是否注入）。
func (r *Relay) SetUsageSink(s usageSink) { r.usageSink = s }

// Tracker 返回回合追踪器（可能为 nil）。session_service / handler 在发送
// session/prompt 前用它登记回合 id。
func (r *Relay) Tracker() *TurnTracker { return r.tracker }

// Subscribe 注册一个 WS tab 订阅。
func (r *Relay) Subscribe(userID uuid.UUID) *Subscriber {
	sub := &Subscriber{
		ID:     uuid.New(),
		UserID: userID,
		Send:   make(chan *EventEnvelope, r.cfg.WSSendBuffer),
	}
	r.subsMu.Lock()
	r.subs[sub.ID] = sub
	r.subsMu.Unlock()
	return sub
}

// Unsubscribe 移除订阅；幂等。
func (r *Relay) Unsubscribe(subID uuid.UUID) {
	r.subsMu.Lock()
	defer r.subsMu.Unlock()
	if sub, ok := r.subs[subID]; ok {
		if sub.closed.CompareAndSwap(false, true) {
			close(sub.Send)
		}
		delete(r.subs, subID)
	}
}

// fanout 把一条 envelope 推给所有 subscribers。慢消费者按 §7.6 自动断开。
func (r *Relay) fanout(env *EventEnvelope) {
	r.subsMu.Lock()
	defer r.subsMu.Unlock()
	for id, sub := range r.subs {
		select {
		case sub.Send <- env:
		default:
			// buffer 满 → 慢消费者，标 closed + 通知 + close channel
			if sub.closed.CompareAndSwap(false, true) {
				disc := &EventEnvelope{Kind: "slow_consumer_disconnect"}
				select {
				case sub.Send <- disc:
				default:
				}
				close(sub.Send)
				delete(r.subs, id)
				r.log.Warn("acp.relay.slow_subscriber_dropped",
					"session_id", r.sessionID, "sub_id", id)
			}
		}
	}
}

// Close 优雅关闭：先广播 final event，再关 channel，再标 closed。
// supervisor.onExit 调用，确保所有 WS 看到 session_terminated。
func (r *Relay) Close(status string, exitCode *int32) {
	if !r.closed.CompareAndSwap(false, true) {
		return
	}
	final := &EventEnvelope{
		Kind: "session_terminated",
		Payload: mustJSON(map[string]any{
			"status":    status,
			"exit_code": exitCode,
		}),
	}
	r.fanout(final)

	r.subsMu.Lock()
	for id, sub := range r.subs {
		if sub.closed.CompareAndSwap(false, true) {
			close(sub.Send)
		}
		delete(r.subs, id)
	}
	r.subsMu.Unlock()

	close(r.done)
}

// FanoutControl 把一条控制类 envelope（如 permission_request/permission_resolved）
// 推给所有 WS 订阅者。这类帧不落 acp_events、ID 恒为 0，由 PermissionService 调用。
// relay 已关闭时静默丢弃，避免向已关 channel 写入。
func (r *Relay) FanoutControl(env *EventEnvelope) {
	if r.closed.Load() {
		return
	}
	r.fanout(env)
}

// Done returns a channel closed when Relay is terminated.
func (r *Relay) Done() <-chan struct{} { return r.done }

func mustJSON(v any) json.RawMessage {
	b, err := json.Marshal(v)
	if err != nil {
		return json.RawMessage(fmt.Sprintf(`{"_marshal_err":%q}`, err.Error()))
	}
	return b
}

// Run is the relay main loop; supervisor.Spawn calls it in a goroutine.
// It launches the reader and writer goroutines and waits for both.
func (r *Relay) Run(ctx context.Context, sess handlers.SessionContext) {
	var wg sync.WaitGroup
	wg.Add(2)
	go func() { defer wg.Done(); r.reader(ctx, sess) }()
	go func() { defer wg.Done(); r.writer(ctx) }()
	wg.Wait()
}

func (r *Relay) reader(ctx context.Context, sess handlers.SessionContext) {
	for {
		msg, err := r.dec.DecodeMessage()
		if err != nil {
			if errors.Is(err, io.EOF) {
				r.log.Info("acp.relay.eof", "session_id", r.sessionID)
				return
			}
			r.log.Error("acp.relay.decode", "session_id", r.sessionID, "err", err.Error())
			return
		}

		envelope := r.persistAndEnvelope(ctx, msg, "out")
		if envelope != nil {
			r.fanout(envelope)
			// 成本核算：在事件落库后用持久化的 event id 喂给 usageSink。usageSink
			// 内部仅在真实可计费回合时累加，并在预算突破时以 detached goroutine 触发
			// kill（绝不阻塞 reader）。
			if r.usageSink != nil {
				r.usageSink.Observe(ctx, msg, envelope.ID)
			}
		}

		switch {
		case msg.IsResponse():
			if id, ok := msg.IDInt64(); ok {
				if ch, hit := r.inflight.LoadAndDelete(id); hit {
					ch.(chan *Message) <- msg
				} else {
					r.log.Warn("acp.relay.orphan_response", "session_id", r.sessionID, "id", id)
				}
			} else if sid, ok := msg.IDString(); ok && r.tracker.IsTrackedPrompt(sid) {
				// 字符串-id 的 session/prompt 响应：回合完成检测。
				// 仅当 id 匹配当前开放回合时拦截；其它字符串 id 仍走既有 fanout（上方已 fanout）。
				if msg.Error != nil {
					// JSON-RPC error 响应：本回合失败，标 aborted 并发 session_idle（reason=error），
					// 而非误记为 completed（stopReason 空）。agent 仍存活，可接受下一条 prompt。
					r.tracker.Abort(ctx, "error")
				} else {
					r.tracker.OnPromptResponse(ctx, sid, ParseStopReason(msg.Result))
				}
			}
			// 其它 String IDs go to clients via fanout (handled above); no server action.
		case msg.IsNotification():
			// Already persisted + fanout'd. No reply. 回合检测：累加 update 计数。
			if msg.Method == "session/update" {
				r.tracker.OnUpdate(ctx)
			}
		case msg.IsRequest():
			go r.handleAgentRequest(ctx, sess, msg)
		}
	}
}

// handleAgentRequest dispatches an agent → server request to the appropriate handler.
// 在独立 goroutine 中运行，故 defer recover 兜底任何 handler panic：记录日志并回送
// JSON-RPC internal error，避免 agent 构造的请求 panic 掉整个进程（DoS）。
func (r *Relay) handleAgentRequest(ctx context.Context, sess handlers.SessionContext, req *Message) {
	defer func() {
		if rec := recover(); rec != nil {
			r.log.Error("acp.relay.handle_request_panic",
				"session_id", r.sessionID, "method", req.Method, "panic", fmt.Sprintf("%v", rec))
			resp := NewResponseErr(req.ID, JSONRPCInternalError, "internal error")
			select {
			case r.serverInitiated <- resp:
			case <-ctx.Done():
			}
		}
	}()

	var resp *Message
	switch req.Method {
	case "fs/read_text_file":
		out := r.fsHandler.Read.Handle(ctx, sess, req.Params)
		resp = r.fsResultToResponse(req.ID, out)
	case "fs/write_text_file":
		out := r.fsHandler.Write.Handle(ctx, sess, req.Params)
		resp = r.fsResultToResponse(req.ID, out)
	case "fs/list":
		out := r.fsHandler.List.Handle(ctx, sess, req.Params)
		resp = r.fsResultToResponse(req.ID, out)
	case "fs/grep":
		out := r.fsHandler.Grep.Handle(ctx, sess, req.Params)
		resp = r.fsResultToResponse(req.ID, out)
	case "session/request_permission":
		out := r.permHandler.Handle(ctx, sess, string(req.ID), req.Params)
		resp = r.fsResultToResponse(req.ID, out)
	default:
		resp = NewResponseErr(req.ID, JSONRPCMethodNotFound, "method not implemented: "+req.Method)
	}
	select {
	case r.serverInitiated <- resp:
	case <-ctx.Done():
	}
}

func (r *Relay) fsResultToResponse(id json.RawMessage, res handlers.FsResult) *Message {
	if res.Err != nil {
		return NewResponseErr(id, res.Err.Code, res.Err.Message)
	}
	return &Message{JSONRPC: "2.0", ID: id, Result: res.OK}
}

func (r *Relay) writer(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		case <-r.done:
			return
		case msg := <-r.serverInitiated:
			r.persistAndEnvelope(ctx, msg, "in")
			if err := r.enc.Encode(msg); err != nil {
				r.log.Error("acp.relay.encode", "session_id", r.sessionID, "err", err.Error())
				return
			}
		case msg := <-r.clientInitiated:
			r.persistAndEnvelope(ctx, msg, "in")
			if err := r.enc.Encode(msg); err != nil {
				r.log.Error("acp.relay.encode", "session_id", r.sessionID, "err", err.Error())
				return
			}
		}
	}
}

// SendClient is the WS handler entry point for forwarding prompt/cancel from a tab.
func (r *Relay) SendClient(msg *Message) error {
	if r.closed.Load() {
		return fmt.Errorf("relay closed")
	}
	select {
	case r.clientInitiated <- msg:
		return nil
	default:
		return fmt.Errorf("relay client channel full")
	}
}

// Call performs a server-initiated request and waits for the response (e.g.
// initialize / session/new). Used by supervisor during handshake.
func (r *Relay) Call(ctx context.Context, method string, params any) (*Message, error) {
	id := r.nextID.Add(1)
	req, err := NewRequestInt(id, method, params)
	if err != nil {
		return nil, err
	}
	ch := make(chan *Message, 1)
	r.inflight.Store(id, ch)
	defer r.inflight.Delete(id)

	select {
	case r.serverInitiated <- req:
	case <-ctx.Done():
		return nil, ctx.Err()
	}

	select {
	case resp := <-ch:
		if resp.Error != nil {
			return resp, resp.Error
		}
		return resp, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

// persistAndEnvelope writes one event to the repo and returns the envelope for
// fanout. Returns nil if the write fails (logged).
func (r *Relay) persistAndEnvelope(ctx context.Context, msg *Message, direction string) *EventEnvelope {
	rpcKind := classifyKind(msg)
	method := msg.Method
	payload, _ := json.Marshal(msg)

	origSize := len(payload)
	truncated := false
	if origSize > r.cfg.EventMaxPayload {
		payload, truncated = truncateLargeFields(msg, r.cfg.EventTruncateField, r.cfg.EventMaxPayload)
	}

	ev, err := r.repo.InsertEvent(ctx, &Event{
		SessionID:   r.sessionID,
		Direction:   EventDirection(direction),
		RPCKind:     RPCKind(rpcKind),
		Method:      ptrIfNotEmpty(method),
		Payload:     payload,
		PayloadSize: int32(origSize),
		Truncated:   truncated,
	})
	if err != nil {
		r.log.Error("acp.relay.persist_event", "session_id", r.sessionID, "err", err.Error())
		return nil
	}
	return &EventEnvelope{
		ID:        ev.ID,
		Direction: direction,
		Kind:      rpcKind,
		Method:    method,
		Payload:   payload,
		Truncated: truncated,
	}
}

func classifyKind(m *Message) string {
	switch {
	case m.IsRequest():
		return string(RPCRequest)
	case m.IsNotification():
		return string(RPCNotification)
	case m.IsResponse():
		if m.Error != nil {
			return string(RPCError)
		}
		return string(RPCResponse)
	}
	return "unknown"
}

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

// truncateLargeFields replaces an oversize payload with a placeholder summary.
// Spec §3.5. MVP: full-payload truncate (don't try to be clever about which
// field is large — agents may put data in many places).
func truncateLargeFields(m *Message, fieldMax, totalMax int) ([]byte, bool) {
	original, _ := json.Marshal(m)
	if len(original) <= totalMax {
		return original, false
	}
	hash := simpleSHA256Hex(original)
	placeholder := fmt.Sprintf("%s... [TRUNCATED size=%d sha256=%s]",
		string(original[:fieldMax]), len(original), hash)
	out, _ := json.Marshal(map[string]any{
		"_truncated_payload": placeholder,
		"_method":            m.Method,
	})
	return out, true
}

func simpleSHA256Hex(b []byte) string {
	h := sha256.Sum256(b)
	return hex.EncodeToString(h[:])
}
