// handler.go 暴露 workspace 模块 HTTP 端点（spec 4.1–4.4 节）。沿 PM 模块（internal/project）
// 的 chi.Router 模式：单个 Handler 持有 3 个 service，Mount 用 sub-router 注册路由。
//
// 鉴权：复用 transport/http/middleware.Auth + 窄接口 AdminLookup 解析 is_admin。
// 错误格式：复用 httpx.WriteError 输出 Google API 风格 {code, message, request_id}。
package workspace

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"strings"

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

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

// AdminLookup 根据 user id 判 is_admin。
// 沿 internal/project 同名窄接口，避免对 user 包具体类型的强依赖（也方便测试注入桩）。
type AdminLookup interface {
	IsAdmin(ctx context.Context, id uuid.UUID) (bool, error)
}

// Handler 同时持有 3 个 service，并在 Mount 时装上 Auth 中间件。
type Handler struct {
	ws       WorkspaceService
	wt       WorktreeService
	gop      GitOpsService
	resolver middleware.SessionResolver
	users    AdminLookup
}

// NewHandler 构造 Handler。resolver 与 users 在 Mount 注册的 Auth 中间件中使用。
func NewHandler(ws WorkspaceService, wt WorktreeService, gop GitOpsService, resolver middleware.SessionResolver, users AdminLookup) *Handler {
	return &Handler{ws: ws, wt: wt, gop: gop, resolver: resolver, users: users}
}

// Mount 把所有 workspace / worktree / gitops 路由挂到 r 上，统一装入 Auth 中间件。
func (h *Handler) Mount(r chi.Router) {
	auth := middleware.Auth(h.resolver, middleware.AuthOptions{})

	r.With(auth).Route("/api/v1/projects/{projectSlug}/workspaces", func(r chi.Router) {
		r.Post("/", h.createWorkspace)
		r.Get("/", h.listWorkspaces)
	})
	r.With(auth).Route("/api/v1/workspaces/{wsID}", func(r chi.Router) {
		r.Get("/", h.getWorkspace)
		r.Patch("/", h.updateWorkspace)
		r.Delete("/", h.deleteWorkspace)
		r.Post("/sync", h.syncWorkspace)
		r.Put("/credential", h.upsertCredential)
		r.Get("/credential", h.getCredentialMeta)

		r.Get("/main/status", h.statusOnMain)
		r.Post("/main/commit", h.commitOnMain)
		r.Post("/main/push", h.pushOnMain)
		r.Get("/main/log", h.logOnMain)
		r.Get("/diff", h.diffOnMain)

		r.Get("/worktrees", h.listWorktrees)
		r.Post("/worktrees", h.createWorktree)
		r.Get("/branches", h.listBranches)
	})
	r.With(auth).Route("/api/v1/worktrees/{wtID}", func(r chi.Router) {
		r.Delete("/", h.deleteWorktree)
		r.Post("/acquire", h.acquireWorktree)
		r.Post("/release", h.releaseWorktree)
		r.Get("/status", h.statusOnWorktree)
		r.Post("/commit", h.commitOnWorktree)
		r.Post("/push", h.pushOnWorktree)
		r.Get("/log", h.logOnWorktree)
		r.Get("/diff", h.diffOnWorktree)
	})
}

// caller 从 ctx 取出 uid 并查 is_admin，组装 Caller。
// Auth 中间件已经把 401 兜在前面；此处的"未认证"检查是双保险。
func (h *Handler) caller(r *http.Request) (Caller, error) {
	uid, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		return Caller{}, errs.New(errs.CodeUnauthorized, "未认证")
	}
	isAdmin, err := h.users.IsAdmin(r.Context(), uid)
	if err != nil {
		return Caller{}, err
	}
	return Caller{UserID: uid, IsAdmin: isAdmin}, nil
}

func writeJSON(w http.ResponseWriter, status int, body any) {
	httpx.WriteJSON(w, status, body)
}

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

// parseUUID 解析 URL 中的 uuid path 参数；失败时返回 invalid_input。
func parseUUID(s string) (uuid.UUID, error) {
	id, err := uuid.Parse(s)
	if err != nil {
		return uuid.Nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid uuid")
	}
	return id, nil
}

// ===== workspace CRUD =====

func (h *Handler) createWorkspace(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req createWorkspaceReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
		return
	}
	cred := UpsertCredentialInput{
		Kind:     CredentialKind(req.Credential.Kind),
		Username: req.Credential.Username,
		Secret:   []byte(req.Credential.Secret),
	}
	in := CreateWorkspaceInput{
		Slug: req.Slug, Name: req.Name, Description: req.Description,
		GitRemoteURL: req.GitRemoteURL, DefaultBranch: req.DefaultBranch, Credential: cred,
	}
	ws, err := h.ws.Create(r.Context(), c, chi.URLParam(r, "projectSlug"), in)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusCreated, toWorkspaceResp(ws))
}

func (h *Handler) listWorkspaces(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	out, err := h.ws.List(r.Context(), c, chi.URLParam(r, "projectSlug"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	resp := make([]workspaceResp, 0, len(out))
	for _, ws := range out {
		resp = append(resp, toWorkspaceResp(ws))
	}
	writeJSON(w, http.StatusOK, resp)
}

func (h *Handler) getWorkspace(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wsID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	ws, err := h.ws.Get(r.Context(), c, id)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, toWorkspaceResp(ws))
}

func (h *Handler) updateWorkspace(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wsID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req updateWorkspaceReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode"))
		return
	}
	ws, err := h.ws.Update(r.Context(), c, id, UpdateWorkspaceInput{
		Name: req.Name, Description: req.Description, DefaultBranch: req.DefaultBranch,
	})
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, toWorkspaceResp(ws))
}

func (h *Handler) deleteWorkspace(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wsID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	if err := h.ws.Delete(r.Context(), c, id); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) syncWorkspace(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wsID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	ws, err := h.ws.Sync(r.Context(), c, id)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, toWorkspaceResp(ws))
}

// ===== credential =====

func (h *Handler) upsertCredential(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wsID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var body credentialBody
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode"))
		return
	}
	cred, err := h.ws.UpsertCredential(r.Context(), c, id, UpsertCredentialInput{
		Kind: CredentialKind(body.Kind), Username: body.Username, Secret: []byte(body.Secret),
	})
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, toCredentialResp(cred))
}

func (h *Handler) getCredentialMeta(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wsID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	cred, err := h.ws.GetCredentialMeta(r.Context(), c, id)
	if err != nil {
		// "凭据未配置" 视为返回 kind=none + 空 fingerprint
		if isNotFound(err) {
			writeJSON(w, http.StatusOK, credentialResp{Kind: string(CredKindNone)})
			return
		}
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, toCredentialResp(cred))
}

func isNotFound(err error) bool {
	var ae *errs.AppError
	return errors.As(err, &ae) && ae.Code == errs.CodeNotFound
}

// ===== worktree =====

func (h *Handler) listWorktrees(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wsID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	out, err := h.wt.List(r.Context(), c, id)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	resp := make([]worktreeResp, 0, len(out))
	for _, x := range out {
		resp = append(resp, toWorktreeResp(x))
	}
	writeJSON(w, http.StatusOK, resp)
}

func (h *Handler) createWorktree(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wsID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req createWorktreeReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode"))
		return
	}
	wt, err := h.wt.Create(r.Context(), c, id, CreateWorktreeInput{Branch: req.Branch, BaseBranch: req.BaseBranch})
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusCreated, toWorktreeResp(wt))
}

func (h *Handler) listBranches(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wsID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	branches, err := h.ws.ListBranches(r.Context(), c, id)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, branchListResp{Branches: branches})
}

func (h *Handler) deleteWorktree(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wtID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	if err := h.wt.Delete(r.Context(), c, id); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) acquireWorktree(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wtID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	wt, err := h.wt.Acquire(r.Context(), c, id)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, toWorktreeResp(wt))
}

func (h *Handler) releaseWorktree(w http.ResponseWriter, r *http.Request) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "wtID"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	wt, err := h.wt.Release(r.Context(), c, id)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, toWorktreeResp(wt))
}

// ===== git ops =====

func (h *Handler) statusOnMain(w http.ResponseWriter, r *http.Request) {
	h.statusGeneric(w, r, true)
}

func (h *Handler) statusOnWorktree(w http.ResponseWriter, r *http.Request) {
	h.statusGeneric(w, r, false)
}

func (h *Handler) statusGeneric(w http.ResponseWriter, r *http.Request, onMain bool) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	param := "wsID"
	if !onMain {
		param = "wtID"
	}
	id, err := parseUUID(chi.URLParam(r, param))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var st []git.FileStatus
	if onMain {
		st, err = h.gop.StatusOnMain(r.Context(), c, id)
	} else {
		st, err = h.gop.StatusOnWorktree(r.Context(), c, id)
	}
	if err != nil {
		writeErr(w, r, err)
		return
	}
	out := make([]fileStatusResp, 0, len(st))
	for _, f := range st {
		out = append(out, toFileStatusResp(f))
	}
	writeJSON(w, http.StatusOK, out)
}

func (h *Handler) commitOnMain(w http.ResponseWriter, r *http.Request) {
	h.commitGeneric(w, r, true)
}

func (h *Handler) commitOnWorktree(w http.ResponseWriter, r *http.Request) {
	h.commitGeneric(w, r, false)
}

func (h *Handler) commitGeneric(w http.ResponseWriter, r *http.Request, onMain bool) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	param := "wsID"
	if !onMain {
		param = "wtID"
	}
	id, err := parseUUID(chi.URLParam(r, param))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req commitReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode"))
		return
	}
	push := req.Push == nil || *req.Push
	addAll := req.AddAll == nil || *req.AddAll
	in := CommitInput{Message: req.Message, Push: push, AddAll: addAll}
	var sha string
	if onMain {
		sha, err = h.gop.CommitOnMain(r.Context(), c, id, in)
	} else {
		sha, err = h.gop.CommitOnWorktree(r.Context(), c, id, in)
	}
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, commitResp{SHA: sha})
}

func (h *Handler) pushOnMain(w http.ResponseWriter, r *http.Request) {
	h.pushGeneric(w, r, true)
}

func (h *Handler) pushOnWorktree(w http.ResponseWriter, r *http.Request) {
	h.pushGeneric(w, r, false)
}

func (h *Handler) pushGeneric(w http.ResponseWriter, r *http.Request, onMain bool) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	param := "wsID"
	if !onMain {
		param = "wtID"
	}
	id, err := parseUUID(chi.URLParam(r, param))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	if onMain {
		err = h.gop.PushOnMain(r.Context(), c, id)
	} else {
		err = h.gop.PushOnWorktree(r.Context(), c, id)
	}
	if err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) logOnMain(w http.ResponseWriter, r *http.Request) {
	h.logGeneric(w, r, true)
}

func (h *Handler) logOnWorktree(w http.ResponseWriter, r *http.Request) {
	h.logGeneric(w, r, false)
}

func (h *Handler) logGeneric(w http.ResponseWriter, r *http.Request, onMain bool) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	param := "wsID"
	if !onMain {
		param = "wtID"
	}
	id, err := parseUUID(chi.URLParam(r, param))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	n := parseLogN(r)
	var commits []git.Commit
	if onMain {
		commits, err = h.gop.LogOnMain(r.Context(), c, id, n)
	} else {
		commits, err = h.gop.LogOnWorktree(r.Context(), c, id, n)
	}
	if err != nil {
		writeErr(w, r, err)
		return
	}
	resp := make([]commitLogResp, 0, len(commits))
	for _, cc := range commits {
		resp = append(resp, toCommitLogResp(cc))
	}
	writeJSON(w, http.StatusOK, resp)
}

func parseLogN(r *http.Request) int {
	nStr := r.URL.Query().Get("n")
	if nStr == "" {
		return 50
	}
	n, err := strconv.Atoi(nStr)
	if err != nil || n <= 0 {
		return 50
	}
	if n > 200 {
		return 200
	}
	return n
}

func (h *Handler) diffOnMain(w http.ResponseWriter, r *http.Request) {
	h.diffGeneric(w, r, true)
}

func (h *Handler) diffOnWorktree(w http.ResponseWriter, r *http.Request) {
	h.diffGeneric(w, r, false)
}

func (h *Handler) diffGeneric(w http.ResponseWriter, r *http.Request, onMain bool) {
	c, err := h.caller(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	param := "wsID"
	if !onMain {
		param = "wtID"
	}
	id, err := parseUUID(chi.URLParam(r, param))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	opts := parseDiffOptions(r)
	var res git.DiffResult
	if onMain {
		res, err = h.gop.DiffOnMain(r.Context(), c, id, opts)
	} else {
		res, err = h.gop.DiffOnWorktree(r.Context(), c, id, opts)
	}
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, diffResp{Diff: res.Diff, Truncated: res.Truncated})
}

// parseDiffOptions reads the diff query params (ref, against, staged, paths, context).
func parseDiffOptions(r *http.Request) git.DiffOptions {
	q := r.URL.Query()
	opts := git.DiffOptions{
		Ref:     q.Get("ref"),
		Against: q.Get("against"),
		Staged:  q.Get("staged") == "true" || q.Get("staged") == "1",
	}
	if raw := q.Get("paths"); raw != "" {
		for _, p := range strings.Split(raw, ",") {
			if p = strings.TrimSpace(p); p != "" {
				opts.Paths = append(opts.Paths, p)
			}
		}
	}
	if cl := q.Get("context"); cl != "" {
		if n, err := strconv.Atoi(cl); err == nil && n >= 0 {
			opts.ContextLines = n
		}
	}
	return opts
}
