package externaladmin

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/middleware"
	"hyapp-admin-server/internal/model"
	"hyapp-admin-server/internal/modules/shared"
	"hyapp-admin-server/internal/response"

	"github.com/gin-gonic/gin"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"gorm.io/gorm"
	userv1 "hyapp.local/api/proto/user/v1"
)

type Handler struct {
	service  *Service
	cfg      Config
	audit    shared.OperationLogger
	userHost userv1.UserHostServiceClient
}

type HandlerOption func(*Handler)

// WithUserHostClient enables only the external portal's explicitly whitelisted
// owner-service reads. The raw client is not exposed to generic proxy routing.
func WithUserHostClient(client userv1.UserHostServiceClient) HandlerOption {
	return func(handler *Handler) {
		handler.userHost = client
	}
}

func WithLoginRateLimiter(limiter FixedWindowLimiter) HandlerOption {
	return func(handler *Handler) {
		handler.service.limiter = limiter
	}
}

func New(db *gorm.DB, userDB *sql.DB, cfg Config, audit shared.OperationLogger, options ...HandlerOption) *Handler {
	handler := &Handler{service: NewService(db, userDB, cfg), cfg: cfg, audit: audit}
	for _, option := range options {
		if option != nil {
			option(handler)
		}
	}
	return handler
}

const (
	myTeamAgencyRPCPageSize = int32(5000)
	myTeamAgencyRPCTimeout  = 5 * time.Second
	maxLoginBodyBytes       = int64(8 << 10)
)

type createAccountRequest struct {
	TargetUserID    FlexibleString              `json:"targetUserId" binding:"required"`
	Username        string                      `json:"username" binding:"required"`
	Password        string                      `json:"password" binding:"required"`
	Permissions     optionalPermissionSelection `json:"permissions"`
	IdentityType    string                      `json:"identityType"`
	ParentAccountID *uint64                     `json:"parentAccountId"`
}

type statusRequest struct {
	Status string `json:"status" binding:"required"`
}

type passwordRequest struct {
	Password string `json:"password" binding:"required"`
}

type updateParentRequest struct {
	ParentAccountID  *uint64 `json:"parentAccountId"`
	ExpectedRevision uint64  `json:"expectedRevision" binding:"required"`
}

type loginRequest struct {
	Account  string `json:"account"`
	Username string `json:"username"`
	Password string `json:"password"`
}

type changePasswordRequest struct {
	CurrentPassword string `json:"currentPassword"`
	OldPassword     string `json:"oldPassword"`
	NewPassword     string `json:"newPassword" binding:"required"`
}

func (h *Handler) ListAccounts(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSizeRaw := c.Query("pageSize")
	if pageSizeRaw == "" {
		pageSizeRaw = c.DefaultQuery("page_size", "20")
	}
	pageSize, _ := strconv.Atoi(pageSizeRaw)
	result, err := h.service.ListAccounts(c.Request.Context(), ListInput{
		AppCode: appctx.FromContext(c.Request.Context()), Page: page, PageSize: pageSize,
		Keyword: c.Query("keyword"), Status: c.Query("status"),
	})
	if err != nil {
		response.ServerError(c, "获取外管用户失败")
		return
	}
	response.OK(c, result)
}

func (h *Handler) ResolveTarget(c *gin.Context) {
	target := strings.TrimSpace(c.Query("display_user_id"))
	if target == "" {
		target = strings.TrimSpace(c.Query("displayUserId"))
	}
	user, err := h.service.ResolveTarget(c.Request.Context(), appctx.FromContext(c.Request.Context()), target)
	if errors.Is(err, ErrInvalidInput) {
		response.BadRequest(c, "请输入用户短 ID")
		return
	}
	if errors.Is(err, ErrTargetNotFound) {
		response.NotFound(c, "当前 App 未找到该用户")
		return
	}
	if err != nil {
		response.ServerError(c, "查询用户失败")
		return
	}
	existing, err := h.service.FindAccountByLinkedUser(c.Request.Context(), appctx.FromContext(c.Request.Context()), user)
	if err != nil {
		response.ServerError(c, "查询外管用户绑定失败")
		return
	}
	response.OK(c, gin.H{"linkedUser": user, "existingExternalAdminUser": existing})
}

func (h *Handler) CreateAccount(c *gin.Context) {
	var request createAccountRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		response.BadRequest(c, "创建参数不正确")
		return
	}
	// Every created account receives a permission snapshot, including the legacy
	// omitted-field shape which expands to the full default. Recheck delegation here
	// as defense in depth for direct handler mounting and future route refactors.
	if !middleware.HasPermission(c, "external-admin-user:permissions") {
		response.Forbidden(c, "没有配置外管用户权限的权限")
		return
	}
	account, err := h.service.CreateAccount(c.Request.Context(), CreateInput{
		AppCode: appctx.FromContext(c.Request.Context()), TargetUserID: string(request.TargetUserID),
		Username: request.Username, Password: request.Password, Permissions: request.Permissions.Value,
		PermissionsSet: request.Permissions.Set, IdentityType: request.IdentityType,
		ParentAccountID: request.ParentAccountID, CreatedByAdminID: middleware.CurrentUserID(c),
	})
	if errors.Is(err, ErrAccountConflict) {
		response.Conflict(c, "外管账户名称已被使用，或当前 App 的绑定用户已存在")
		return
	}
	if errors.Is(err, ErrTargetNotFound) {
		response.NotFound(c, "当前 App 未找到该用户")
		return
	}
	if errors.Is(err, ErrPasswordBlank) {
		response.BadRequest(c, "密码不能全部为空白字符")
		return
	}
	if errors.Is(err, ErrInvalidPermissions) {
		response.BadRequest(c, permissionValidationMessage(err))
		return
	}
	if errors.Is(err, ErrInvalidHierarchy) {
		response.BadRequest(c, "外管身份、上级或当前区域不符合层级规则")
		return
	}
	if errors.Is(err, ErrInvalidInput) || strings.Contains(fmt.Sprint(err), "password length") {
		response.BadRequest(c, "账户名称格式不正确，或密码超过 72 字节")
		return
	}
	if err != nil {
		response.ServerError(c, "创建外管用户失败")
		return
	}
	shared.OperationLogWithResourceID(c, h.audit, "create-external-admin-user", "external_admin_accounts", strconv.FormatUint(account.ID, 10), "success", fmt.Sprintf(
		"app_code=%s linked_user_id=%d username=%s identity_type=%s parent_account_id=%s permission_count=%d",
		account.AppCode, account.LinkedUser.UserID, account.Username, account.IdentityType, optionalAccountID(account.ParentAccountID), len(account.Permissions),
	))
	response.Created(c, account)
}

func (h *Handler) ListParentCandidates(c *gin.Context) {
	childAccountID, _ := strconv.ParseUint(strings.TrimSpace(c.Query("accountId")), 10, 64)
	targetUserID := strings.TrimSpace(c.Query("targetUserId"))
	if targetUserID == "" {
		targetUserID = strings.TrimSpace(c.Query("displayUserId"))
	}
	result, err := h.service.ListParentCandidates(c.Request.Context(), ParentCandidateInput{
		AppCode: appctx.FromContext(c.Request.Context()), IdentityType: c.Query("identityType"),
		TargetUserID: targetUserID, ChildAccountID: childAccountID, Keyword: c.Query("keyword"),
	})
	if errors.Is(err, ErrAccountNotFound) || errors.Is(err, ErrTargetNotFound) {
		response.NotFound(c, "外管用户或绑定 App 用户不存在")
		return
	}
	if errors.Is(err, ErrInvalidInput) {
		response.BadRequest(c, "请选择有效身份和绑定用户")
		return
	}
	if err != nil {
		response.ServerError(c, "获取外管上级候选失败")
		return
	}
	response.OK(c, result)
}

func (h *Handler) UpdateParent(c *gin.Context) {
	id, ok := parseUint64Param(c, "id")
	if !ok {
		return
	}
	var request updateParentRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		response.BadRequest(c, "上级绑定参数不正确")
		return
	}
	result, err := h.service.UpdateAccountParent(c.Request.Context(), appctx.FromContext(c.Request.Context()), id, UpdateParentInput{
		ParentAccountID: request.ParentAccountID, ExpectedRevision: request.ExpectedRevision,
	})
	if errors.Is(err, ErrParentConflict) {
		response.Conflict(c, "外管用户已被其他管理员修改，请刷新后重试")
		return
	}
	if errors.Is(err, ErrAccountNotFound) {
		response.NotFound(c, "外管用户不存在")
		return
	}
	if errors.Is(err, ErrInvalidHierarchy) {
		response.BadRequest(c, "上级身份、状态或区域不符合层级规则")
		return
	}
	if errors.Is(err, ErrInvalidInput) {
		response.BadRequest(c, "上级绑定参数不正确")
		return
	}
	if err != nil {
		response.ServerError(c, "绑定外管用户上级失败")
		return
	}
	shared.OperationLogWithResourceID(
		c, h.audit, "update-external-admin-user-parent", "external_admin_accounts", strconv.FormatUint(id, 10), "success",
		fmt.Sprintf("parent_account_id=%s revision=%d changed=%t sessions_revoked=%t", optionalAccountID(request.ParentAccountID), result.Account.PermissionRevision, result.Changed, result.SessionsRevoked),
	)
	response.OK(c, result)
}

func (h *Handler) ListExternalSuperAdmins(c *gin.Context) {
	principal, ok := CurrentPrincipal(c)
	if !ok {
		response.Unauthorized(c, "外管会话已失效")
		return
	}
	if normalizeIdentity(principal.IdentityType) == model.ExternalAdminIdentityExternalSuperAdmin {
		response.Forbidden(c, "外管SuperAdmin不能查看此菜单")
		return
	}
	result, err := h.service.ListExternalSuperAdmins(c.Request.Context(), principal)
	if errors.Is(err, ErrInvalidHierarchy) {
		response.Forbidden(c, "当前身份不能查看外管SuperAdmin")
		return
	}
	if err != nil {
		response.ServerError(c, "获取外管SuperAdmin失败")
		return
	}
	response.OK(c, result)
}

func (h *Handler) SetStatus(c *gin.Context) {
	id, ok := parseUint64Param(c, "id")
	if !ok {
		return
	}
	var request statusRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		response.BadRequest(c, "状态参数不正确")
		return
	}
	account, err := h.service.SetStatus(c.Request.Context(), appctx.FromContext(c.Request.Context()), id, request.Status)
	if errors.Is(err, ErrInvalidInput) {
		response.BadRequest(c, "状态仅支持 active 或 disabled")
		return
	}
	if errors.Is(err, ErrAccountNotFound) {
		response.NotFound(c, "外管用户不存在")
		return
	}
	if err != nil {
		response.ServerError(c, "更新外管用户状态失败")
		return
	}
	shared.OperationLogWithResourceID(c, h.audit, "update-external-admin-user-status", "external_admin_accounts", strconv.FormatUint(id, 10), "success", fmt.Sprintf("app_code=%s status=%s sessions_revoked=%t", account.AppCode, account.Status, account.Status == "disabled"))
	response.OK(c, account)
}

func (h *Handler) ResetPassword(c *gin.Context) {
	id, ok := parseUint64Param(c, "id")
	if !ok {
		return
	}
	var request passwordRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		response.BadRequest(c, "密码参数不正确")
		return
	}
	account, err := h.service.ResetPassword(c.Request.Context(), appctx.FromContext(c.Request.Context()), id, request.Password)
	if errors.Is(err, ErrPasswordBlank) {
		response.BadRequest(c, "密码不能全部为空白字符")
		return
	}
	if errors.Is(err, ErrInvalidInput) || strings.Contains(fmt.Sprint(err), "password length") {
		response.BadRequest(c, "密码不能超过 72 字节")
		return
	}
	if errors.Is(err, ErrAccountNotFound) {
		response.NotFound(c, "外管用户不存在")
		return
	}
	if err != nil {
		response.ServerError(c, "重置外管用户密码失败")
		return
	}
	shared.OperationLogWithResourceID(c, h.audit, "reset-external-admin-user-password", "external_admin_accounts", strconv.FormatUint(id, 10), "success", fmt.Sprintf("app_code=%s sessions_revoked=true", account.AppCode))
	response.OK(c, account)
}

func (h *Handler) Login(c *gin.Context) {
	// The global App middleware supplies the main-admin default before this anonymous
	// route runs. No tenant is authoritative until a credential row resolves, so do
	// not leak or imply that default on malformed, throttled or failed login responses.
	c.Writer.Header().Del(appctx.HeaderAppCode)
	// Limit parsing memory before decoding untrusted credentials. Binding tags are
	// intentionally absent on loginRequest: every syntactically valid JSON attempt,
	// including missing fields, must reach the distributed limiter before rejection.
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxLoginBodyBytes)
	var request loginRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		var maxBytesErr *http.MaxBytesError
		if errors.As(err, &maxBytesErr) {
			response.PayloadTooLarge(c, "登录请求体不能超过 8 KiB")
			return
		}
		response.BadRequest(c, "登录参数不正确")
		return
	}
	username := strings.TrimSpace(request.Account)
	if username == "" {
		username = request.Username
	}
	result, err := h.service.Login(c.Request.Context(), LoginInput{
		Username: username, Password: request.Password,
		IP: c.ClientIP(), UserAgent: c.Request.UserAgent(),
	})
	var rateLimitErr *LoginRateLimitError
	if errors.As(err, &rateLimitErr) {
		retrySeconds := int64((rateLimitErr.RetryAfter + time.Second - 1) / time.Second)
		if retrySeconds < 1 {
			retrySeconds = 1
		}
		c.Header("Retry-After", strconv.FormatInt(retrySeconds, 10))
		response.TooManyRequests(c, "登录尝试过于频繁，请稍后重试")
		return
	}
	if errors.Is(err, ErrLoginLimiterFailed) {
		response.ServiceUnavailable(c, "登录安全服务暂不可用")
		return
	}
	if errors.Is(err, ErrInvalidCredentials) {
		// App is deliberately absent from both input and error text. The account row is
		// the only tenant authority, while the stable 401/40100 pair lets every locale
		// render its own generic message without parsing this server-side fallback.
		response.Unauthorized(c, "账号或密码错误")
		return
	}
	if err != nil {
		response.ServerError(c, "登录服务暂不可用")
		return
	}
	// The global App middleware cannot know the tenant before authentication and
	// therefore writes its generic default header. Replace it with the App resolved
	// from the credential row so the successful response has one consistent tenant.
	c.Header(appctx.HeaderAppCode, result.View.AppCode)
	h.setSessionCookies(c, result.SessionToken, result.CSRFToken)
	c.Header(CSRFHeaderName, result.CSRFToken)
	response.OK(c, result.View)
}

func (h *Handler) Me(c *gin.Context) {
	principal, ok := CurrentPrincipal(c)
	if !ok {
		response.Unauthorized(c, "外管会话已失效")
		return
	}
	csrfToken := CurrentCSRFToken(c)
	view, err := h.service.Me(c.Request.Context(), principal, csrfToken)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, sql.ErrNoRows) || errors.Is(err, ErrTargetNotFound) {
			h.clearSessionCookies(c)
			response.Unauthorized(c, "外管会话已失效")
		} else {
			response.ServerError(c, "获取外管会话失败")
		}
		return
	}
	if csrfToken != "" {
		c.Header(CSRFHeaderName, csrfToken)
	}
	response.OK(c, view)
}

func (h *Handler) Logout(c *gin.Context) {
	principal, _ := CurrentPrincipal(c)
	if err := h.service.RevokeSession(c.Request.Context(), principal.SessionID, "logout"); err != nil {
		response.ServerError(c, "退出登录失败")
		return
	}
	h.clearSessionCookies(c)
	response.OK(c, gin.H{"loggedOut": true})
}

func (h *Handler) ChangePassword(c *gin.Context) {
	principal, ok := CurrentPrincipal(c)
	if !ok {
		response.Unauthorized(c, "外管会话已失效")
		return
	}
	var request changePasswordRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		response.BadRequest(c, "密码参数不正确")
		return
	}
	currentPassword := request.CurrentPassword
	if strings.TrimSpace(currentPassword) == "" {
		currentPassword = request.OldPassword
	}
	err := h.service.ChangePassword(c.Request.Context(), principal, ChangePasswordInput{CurrentPassword: currentPassword, NewPassword: request.NewPassword})
	if errors.Is(err, ErrInvalidCredentials) {
		response.BadRequest(c, "当前密码不正确")
		return
	}
	if errors.Is(err, ErrPasswordReused) {
		response.BadRequest(c, "新密码不能与当前密码相同")
		return
	}
	if errors.Is(err, ErrPasswordBlank) {
		response.BadRequest(c, "新密码不能全部为空白字符")
		return
	}
	if errors.Is(err, ErrInvalidInput) {
		response.BadRequest(c, "新密码不能超过 72 字节")
		return
	}
	if err != nil {
		response.ServerError(c, "修改密码失败")
		return
	}
	response.OK(c, gin.H{"passwordChanged": true})
}

// ListMyTeamAgencies returns only the team rooted at the App user bound to this
// external account. Deliberately do not read manager_user_id/user_id from query or
// path parameters: otherwise a valid external session could enumerate another
// manager's organization tree by changing one client-controlled value.
func (h *Handler) ListMyTeamAgencies(c *gin.Context) {
	principal, ok := CurrentPrincipal(c)
	if !ok || principal.LinkedAppUserID <= 0 {
		response.Unauthorized(c, "外管会话已失效")
		return
	}
	if h.userHost == nil {
		response.ServerError(c, "团队服务暂不可用")
		return
	}

	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSizeRaw := strings.TrimSpace(c.Query("pageSize"))
	if pageSizeRaw == "" {
		pageSizeRaw = c.DefaultQuery("page_size", "20")
	}
	pageSize, _ := strconv.Atoi(pageSizeRaw)
	page, pageSize = normalizePage(page, pageSize)

	// The owner service resolves the complete active manager tree up to its
	// defensive 5,000-row cap. Local slicing keeps total and pagination mutually
	// consistent, while truncated explicitly tells clients when the owner cap hit.
	ctx, cancel := context.WithTimeout(c.Request.Context(), myTeamAgencyRPCTimeout)
	defer cancel()
	result, err := h.userHost.ListManagerTeamAgencies(ctx, &userv1.ListManagerTeamAgenciesRequest{
		Meta: &userv1.RequestMeta{
			RequestId: middleware.CurrentRequestID(c),
			Caller:    "hyapp-admin-server-external",
			SentAtMs:  time.Now().UTC().UnixMilli(),
			AppCode:   principal.AppCode,
			ClientIp:  c.ClientIP(),
			UserAgent: truncateText(c.Request.UserAgent(), 512),
		},
		ManagerUserId: principal.LinkedAppUserID,
		PageSize:      myTeamAgencyRPCPageSize,
	})
	if err != nil {
		if status.Code(err) == codes.PermissionDenied {
			response.Forbidden(c, "当前绑定用户不是有效的团队管理员")
			return
		}
		response.ServerError(c, "获取我的团队失败")
		return
	}

	allItems := make([]MyTeamAgency, 0, len(result.GetAgencies()))
	for _, agency := range result.GetAgencies() {
		if agency == nil || agency.GetAgencyId() <= 0 {
			continue
		}
		allItems = append(allItems, MyTeamAgency{
			AgencyID: agency.GetAgencyId(), OwnerUserID: agency.GetOwnerUserId(),
			ParentBDUserID: agency.GetParentBdUserId(), Status: "active",
		})
	}
	// The owner RPC intentionally returns only relation IDs. Honor the UI's keyword
	// without widening scope or scanning user tables: match only IDs already present
	// in this session-scoped result, then calculate total and pagination from that set.
	allItems = filterMyTeamAgencies(allItems, c.Query("keyword"))
	items := paginateMyTeamAgencies(allItems, page, pageSize)
	response.OK(c, MyTeamAgencyPage{
		Items: items, Page: page, PageSize: pageSize, Total: len(allItems),
		TotalBDLeaders: result.GetTotalBdLeaders(), TotalBDs: result.GetTotalBds(), Truncated: result.GetTruncated(),
	})
}

func filterMyTeamAgencies(items []MyTeamAgency, keyword string) []MyTeamAgency {
	keyword = strings.TrimSpace(keyword)
	if keyword == "" {
		return items
	}
	filtered := make([]MyTeamAgency, 0, len(items))
	for _, item := range items {
		if strings.Contains(strconv.FormatInt(item.AgencyID, 10), keyword) ||
			strings.Contains(strconv.FormatInt(item.OwnerUserID, 10), keyword) ||
			strings.Contains(strconv.FormatInt(item.ParentBDUserID, 10), keyword) {
			filtered = append(filtered, item)
		}
	}
	return filtered
}

func paginateMyTeamAgencies(items []MyTeamAgency, page int, pageSize int) []MyTeamAgency {
	if len(items) == 0 {
		return []MyTeamAgency{}
	}
	lastPage := (len(items) + pageSize - 1) / pageSize
	if page > lastPage {
		return []MyTeamAgency{}
	}
	start := (page - 1) * pageSize
	end := start + pageSize
	if end > len(items) {
		end = len(items)
	}
	return items[start:end]
}

func (h *Handler) setSessionCookies(c *gin.Context, sessionToken string, csrfToken string) {
	maxAge := int(h.cfg.SessionTTL.Seconds())
	if maxAge <= 0 {
		maxAge = int((12 * time.Hour).Seconds())
	}
	h.writeCookie(c, SessionCookieName, sessionToken, true, maxAge, time.Now().UTC().Add(time.Duration(maxAge)*time.Second))
	h.writeCookie(c, CSRFCookieName, csrfToken, false, maxAge, time.Now().UTC().Add(time.Duration(maxAge)*time.Second))
}

func (h *Handler) clearSessionCookies(c *gin.Context) {
	expired := time.Unix(1, 0).UTC()
	h.writeCookie(c, SessionCookieName, "", true, -1, expired)
	h.writeCookie(c, CSRFCookieName, "", false, -1, expired)
}

func (h *Handler) writeCookie(c *gin.Context, name string, value string, httpOnly bool, maxAge int, expires time.Time) {
	http.SetCookie(c.Writer, &http.Cookie{
		Name: name, Value: value, Path: CookiePath, MaxAge: maxAge, Expires: expires,
		HttpOnly: httpOnly, Secure: h.cfg.CookieSecure, SameSite: parseSameSite(h.cfg.CookieSameSite),
	})
}

func parseSameSite(value string) http.SameSite {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "strict":
		return http.SameSiteStrictMode
	case "none":
		return http.SameSiteNoneMode
	default:
		return http.SameSiteLaxMode
	}
}

func parseUint64Param(c *gin.Context, name string) (uint64, bool) {
	id, err := strconv.ParseUint(c.Param(name), 10, 64)
	if err != nil || id == 0 {
		response.BadRequest(c, "ID 参数不正确")
		return 0, false
	}
	return id, true
}

func optionalAccountID(value *uint64) string {
	if value == nil {
		return "none"
	}
	return strconv.FormatUint(*value, 10)
}
