package terminal

import (
	"context"
	"encoding/json"
	"net/http"
	"strconv"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"

	ws "github.com/coder/websocket"

	"github.com/yan1h/agent-coding-workflow/internal/audit"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
	"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
)

// readBufSize 是从 shell 读输出的缓冲块大小。
const readBufSize = 32 * 1024

// wsReadLimit 放宽单条消息上限以容纳较大的粘贴输入（默认 32KB 对粘贴偏小）。
const wsReadLimit = 1 << 20

// AdminLookup 解析用户是否为管理员（与 run/acp 模块同一模式）。
type AdminLookup interface {
	IsAdmin(ctx context.Context, userID uuid.UUID) (bool, error)
}

// WSConfig 控制 WebSocket 心跳。
type WSConfig struct {
	PingInterval time.Duration
	PongTimeout  time.Duration
}

// Handler 暴露仅限管理员的终端 WS 端点。
type Handler struct {
	sup         *Supervisor
	resolver    middleware.SessionResolver
	adminLookup AdminLookup
	audit       audit.Recorder
	shell       string
	cfg         WSConfig
}

// NewHandler 构造 terminal.Handler。
func NewHandler(sup *Supervisor, resolver middleware.SessionResolver, al AdminLookup, rec audit.Recorder, shell string, cfg WSConfig) *Handler {
	return &Handler{sup: sup, resolver: resolver, adminLookup: al, audit: rec, shell: shell, cfg: cfg}
}

// Mount 注册终端路由（仅 admin）。WS 鉴权经 middleware.Auth（浏览器原生 WebSocket
// 无法带自定义 header，token 走 ?token= 查询参数，沿用 run/acp 的 WS 约定）。
func (h *Handler) Mount(r chi.Router) {
	r.Route("/api/v1/admin/terminal", func(r chi.Router) {
		r.Use(middleware.Auth(h.resolver, middleware.AuthOptions{}))
		r.Get("/stream", h.stream)
	})
}

// requireAdmin 从请求解析用户并要求其为管理员，否则返回 forbidden。
func (h *Handler) requireAdmin(r *http.Request) (uuid.UUID, error) {
	uid, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		return uuid.Nil, errs.New(errs.CodeUnauthorized, "not authenticated")
	}
	admin, err := h.adminLookup.IsAdmin(r.Context(), uid)
	if err != nil {
		return uuid.Nil, err
	}
	if !admin {
		return uuid.Nil, errs.New(errs.CodeForbidden, "admin only")
	}
	return uid, nil
}

// resizeMsg 是客户端经文本帧上报的窗口尺寸控制消息。
type resizeMsg struct {
	Type string `json:"type"`
	Rows uint16 `json:"rows"`
	Cols uint16 `json:"cols"`
}

// stream 升级为 WebSocket，开一个 shell 会话并双向桥接：
//   - server→client：shell 输出作为二进制帧推送
//   - client→server：二进制帧 = stdin 原始字节；文本帧 = JSON 控制（resize）
//
// 二进制/文本帧天然区分 stdin 与控制消息，无需额外前缀字节。
func (h *Handler) stream(w http.ResponseWriter, r *http.Request) {
	uid, err := h.requireAdmin(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	rows := uint16(parseUint(r.URL.Query().Get("rows"), uint64(defaultRows)))
	cols := uint16(parseUint(r.URL.Query().Get("cols"), uint64(defaultCols)))

	conn, aerr := ws.Accept(w, r, &ws.AcceptOptions{})
	if aerr != nil {
		return
	}
	defer conn.Close(ws.StatusInternalError, "internal")
	conn.SetReadLimit(wsReadLimit)

	sess, oerr := h.sup.Open(OpenParams{Rows: rows, Cols: cols})
	if oerr != nil {
		conn.Close(ws.StatusTryAgainLater, "cannot open terminal session")
		return
	}

	h.record(r.Context(), r.RemoteAddr, uid, "admin.terminal.opened", sess.ID)
	defer func() {
		// 连接已断时 r.Context() 多半已取消，审计与终止改用独立 context。
		closeCtx, cancel := context.WithTimeout(context.Background(), h.cfg.PongTimeout)
		defer cancel()
		h.sup.Close(closeCtx, sess.ID)
		h.record(closeCtx, r.RemoteAddr, uid, "admin.terminal.closed", sess.ID)
	}()

	ctx, cancel := context.WithCancel(r.Context())
	defer cancel()

	// shell 进程自行退出（如用户输入 exit）→ 取消，触发主循环退出。
	go func() {
		select {
		case <-ctx.Done():
		case <-sess.Done():
			cancel()
		}
	}()

	// read pump：shell 输出 → WS 二进制帧。
	go func() {
		buf := make([]byte, readBufSize)
		for {
			n, rerr := sess.Read(buf)
			if n > 0 {
				if werr := conn.Write(ctx, ws.MessageBinary, buf[:n]); werr != nil {
					cancel()
					return
				}
			}
			if rerr != nil {
				cancel()
				return
			}
		}
	}()

	// 心跳。
	pingTicker := time.NewTicker(h.cfg.PingInterval)
	defer pingTicker.Stop()
	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			case <-pingTicker.C:
				pingCtx, pcancel := context.WithTimeout(ctx, h.cfg.PongTimeout)
				perr := conn.Ping(pingCtx)
				pcancel()
				if perr != nil {
					cancel()
					return
				}
			}
		}
	}()

	// write pump（主循环）：WS → shell stdin / resize。
	for {
		typ, data, rerr := conn.Read(ctx)
		if rerr != nil {
			return
		}
		switch typ {
		case ws.MessageBinary:
			if _, werr := sess.Write(data); werr != nil {
				return
			}
		case ws.MessageText:
			var m resizeMsg
			if json.Unmarshal(data, &m) == nil && m.Type == "resize" {
				_ = sess.Resize(m.Rows, m.Cols)
			}
		}
	}
}

// record 写一条审计日志，吞错（审计失败不应影响终端会话）。
func (h *Handler) record(ctx context.Context, remoteAddr string, uid uuid.UUID, action string, sessID uuid.UUID) {
	if h.audit == nil {
		return
	}
	u := uid
	_ = h.audit.Record(ctx, audit.Entry{
		UserID:     &u,
		Action:     action,
		TargetType: "terminal_session",
		TargetID:   sessID.String(),
		IP:         remoteAddr,
		Metadata:   map[string]any{"shell": h.shell},
	})
}

func parseUint(s string, def uint64) uint64 {
	if s == "" {
		return def
	}
	n, err := strconv.ParseUint(s, 10, 16)
	if err != nil {
		return def
	}
	return n
}

func writeErr(w http.ResponseWriter, r *http.Request, err error) {
	httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err)
}
