package externaladmin

import (
	"crypto/subtle"
	"errors"
	"net/http"
	"strings"
	"time"

	"hyapp-admin-server/internal/appctx"
	adminmiddleware "hyapp-admin-server/internal/middleware"
	"hyapp-admin-server/internal/model"
	"hyapp-admin-server/internal/response"
	"hyapp-admin-server/internal/security"

	"github.com/gin-gonic/gin"
)

const (
	contextPrincipal = "externalAdminPrincipal"
	contextCSRFToken = "externalAdminCSRFToken"
	contextScope     = "externalAdminScope"
)

type RequestScope struct {
	AccountID       uint64
	LinkedAppUserID int64
	IdentityType    string
	RegionID        int64
	OwnerUserIDs    []int64
}

func (h *Handler) AuthRequired() gin.HandlerFunc {
	return func(c *gin.Context) {
		cookie, err := c.Request.Cookie(SessionCookieName)
		if err != nil || strings.TrimSpace(cookie.Value) == "" {
			response.Unauthorized(c, "缺少外管会话")
			c.Abort()
			return
		}
		principal, err := h.service.Authenticate(c.Request.Context(), cookie.Value)
		if err != nil {
			if errors.Is(err, ErrInvalidSession) {
				h.clearSessionCookies(c)
				response.Unauthorized(c, "外管会话已失效")
			} else {
				response.ServerError(c, "外管会话服务暂不可用")
			}
			c.Abort()
			return
		}

		// The session is the tenant authority. A caller may omit X-App-Code, but it can never
		// switch tenants by sending a header that differs from the App fixed at login.
		headerAppCode := strings.ToLower(strings.TrimSpace(c.GetHeader(appctx.HeaderAppCode)))
		if headerAppCode != "" && headerAppCode != principal.AppCode {
			response.Forbidden(c, "请求 App 与外管会话不一致")
			c.Abort()
			return
		}
		requestContext := appctx.WithContext(c.Request.Context(), principal.AppCode)
		requestContext = appctx.WithExternalScope(requestContext, appctx.ExternalScope{
			AccountID: principal.AccountID, LinkedAppUserID: principal.LinkedAppUserID,
			IdentityType: principal.IdentityType, RegionID: principal.RegionID,
			OwnerUserIDs: append([]int64(nil), principal.ScopeOwnerUserIDs...),
		})
		c.Request = c.Request.WithContext(requestContext)
		c.Header(appctx.HeaderAppCode, principal.AppCode)
		c.Set(contextPrincipal, principal)
		c.Set(contextScope, RequestScope{
			AccountID: principal.AccountID, LinkedAppUserID: principal.LinkedAppUserID,
			IdentityType: principal.IdentityType, RegionID: principal.RegionID,
			OwnerUserIDs: append([]int64(nil), principal.ScopeOwnerUserIDs...),
		})
		// Owner services model operators as App users, not admin credential IDs. The
		// isolated external audit log still records AccountID, while downstream facts
		// receive the bound App identity required for team ownership and grant history.
		c.Set(adminmiddleware.ContextUserID, uint(principal.LinkedAppUserID))
		c.Set(adminmiddleware.ContextUsername, operatorUsername(principal.AppCode, principal.Username))
		c.Set(adminmiddleware.ContextPermissions, principal.Permissions)

		// The raw CSRF secret is never persisted server-side. Echo it only when the browser's
		// double-submit cookie still matches the hash stored in this exact session.
		if csrfCookie, cookieErr := c.Request.Cookie(CSRFCookieName); cookieErr == nil && secureEqualHash(csrfCookie.Value, principal.CSRFTokenHash) {
			c.Set(contextCSRFToken, csrfCookie.Value)
		}
		c.Next()
	}
}

// RequireRegionScope protects every external business route with server-derived live
// scope. Authentication and password rotation remain available when an App user has no
// active region, but no business handler can treat region 0 or an invalid parent chain
// as global access.
func (h *Handler) RequireRegionScope() gin.HandlerFunc {
	return func(c *gin.Context) {
		principal, ok := CurrentPrincipal(c)
		if !ok {
			response.Unauthorized(c, "外管会话已失效")
			c.Abort()
			return
		}
		if principal.RegionID <= 0 || len(principal.ScopeOwnerUserIDs) == 0 {
			response.Forbidden(c, "当前外管账号没有有效区域或团队范围")
			c.Abort()
			return
		}
		c.Next()
	}
}

func (h *Handler) RequireCSRF() gin.HandlerFunc {
	return func(c *gin.Context) {
		if isSafeMethod(c.Request.Method) {
			c.Next()
			return
		}
		principal, ok := CurrentPrincipal(c)
		if !ok {
			response.Unauthorized(c, "外管会话已失效")
			c.Abort()
			return
		}
		csrfCookie, cookieErr := c.Request.Cookie(CSRFCookieName)
		headerToken := strings.TrimSpace(c.GetHeader(CSRFHeaderName))
		if cookieErr != nil || headerToken == "" || !secureEqual(csrfCookie.Value, headerToken) || !secureEqualHash(headerToken, principal.CSRFTokenHash) {
			response.Forbidden(c, "CSRF 校验失败")
			c.Abort()
			return
		}
		c.Set(contextCSRFToken, headerToken)
		c.Next()
	}
}

func (h *Handler) RequirePasswordChanged() gin.HandlerFunc {
	// Deprecated helper retained for downstream source compatibility. External routes
	// never install this gate, and session principals always expose the marker as false.
	return func(c *gin.Context) {
		principal, ok := CurrentPrincipal(c)
		if !ok {
			response.Unauthorized(c, "外管会话已失效")
			c.Abort()
			return
		}
		if principal.PasswordChangeRequired {
			response.Forbidden(c, "请先修改初始密码")
			c.Abort()
			return
		}
		c.Next()
	}
}

func (h *Handler) Audit() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		c.Next()
		if isSafeMethod(c.Request.Method) {
			return
		}
		principal, ok := CurrentPrincipal(c)
		if !ok {
			return
		}
		status := "success"
		if c.Writer.Status() >= http.StatusBadRequest {
			status = "failed"
		}
		fullPath := c.FullPath()
		if fullPath == "" {
			fullPath = c.Request.URL.Path
		}
		_ = h.service.CreateOperationLog(c.Request.Context(), model.ExternalAdminOperationLog{
			AccountID: principal.AccountID, AppCode: principal.AppCode, Username: principal.Username,
			RequestID: adminmiddleware.CurrentRequestID(c), Action: strings.ToLower(c.Request.Method) + " " + fullPath,
			Resource: externalAuditResource(fullPath), ResourceID: externalAuditResourceID(c),
			Method: c.Request.Method, Path: c.Request.URL.Path, IP: c.ClientIP(), UserAgent: c.Request.UserAgent(),
			Status: status, HTTPStatus: c.Writer.Status(), LatencyMS: time.Since(start).Milliseconds(),
		})
	}
}

func CurrentPrincipal(c *gin.Context) (SessionPrincipal, bool) {
	value, ok := c.Get(contextPrincipal)
	if !ok {
		return SessionPrincipal{}, false
	}
	principal, ok := value.(SessionPrincipal)
	return principal, ok
}

func CurrentCSRFToken(c *gin.Context) string {
	value, _ := c.Get(contextCSRFToken)
	token, _ := value.(string)
	return token
}

func CurrentScope(c *gin.Context) (RequestScope, bool) {
	value, ok := c.Get(contextScope)
	if !ok {
		return RequestScope{}, false
	}
	scope, ok := value.(RequestScope)
	if !ok {
		return RequestScope{}, false
	}
	scope.OwnerUserIDs = append([]int64(nil), scope.OwnerUserIDs...)
	return scope, true
}

func secureEqual(left string, right string) bool {
	if len(left) == 0 || len(left) != len(right) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(left), []byte(right)) == 1
}

func secureEqualHash(raw string, expectedHash string) bool {
	return secureEqual(security.HashToken(raw), expectedHash)
}

func isSafeMethod(method string) bool {
	switch method {
	case http.MethodGet, http.MethodHead, http.MethodOptions:
		return true
	default:
		return false
	}
}

func externalAuditResourceID(c *gin.Context) string {
	for _, name := range []string{"id", "user_id", "room_id", "resource_id", "grant_id", "pretty_id", "banner_id"} {
		if value := c.Param(name); value != "" {
			return value
		}
	}
	return ""
}

func externalAuditResource(path string) string {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	for index, part := range parts {
		if part == "external" && index+1 < len(parts) {
			return parts[index+1]
		}
	}
	return "external-admin"
}
