package api

import (
	"errors"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"

	"oci-portal/internal/model"

	"oci-portal/internal/service"
)

// authxHandler 处理两步验证与外部身份(OAuth)接口。
type authxHandler struct {
	auth  *service.AuthService
	oauth *service.OAuthService
	logs  *service.SystemLogService
}

// ---- TOTP(JWT 组内) ----

// totpStatus 返回当前账号两步验证启用状态。
//
// @Summary  两步验证状态
// @Tags     认证
// @Success  200 {object} enabledResponse "enabled"
// @Security BearerAuth
// @Router   /api/v1/auth/totp [get]
func (h *authxHandler) totpStatus(c *gin.Context) {
	enabled, err := h.auth.TotpStatus(c.Request.Context(), c.GetString(usernameKey))
	if err != nil {
		respondError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{"enabled": enabled})
}

// totpSetup 生成待激活密钥,返回手动输入码与 otpauth URI(前端渲染二维码)。
//
// @Summary  发起两步验证设置
// @Tags     认证
// @Success  200 {object} totpSetupResponse "secret 与 otpauthUri"
// @Failure  409 {object} errorResponse "已启用"
// @Security BearerAuth
// @Router   /api/v1/auth/totp/setup [post]
func (h *authxHandler) totpSetup(c *gin.Context) {
	secret, uri, err := h.auth.SetupTotp(c.Request.Context(), c.GetString(usernameKey))
	if err != nil {
		if errors.Is(err, service.ErrTotpAlreadyOn) {
			c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
			return
		}
		respondError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{"secret": secret, "otpauthUri": uri})
}

// totpActivate 校验验证码并正式启用两步验证。
//
// @Summary  激活两步验证
// @Tags     认证
// @Param    body body object true "{code: 6 位验证码}"
// @Success  204 "已启用"
// @Security BearerAuth
// @Router   /api/v1/auth/totp/activate [post]
func (h *authxHandler) totpActivate(c *gin.Context) {
	var req struct {
		Code string `json:"code" binding:"required"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	err := h.auth.ActivateTotp(c.Request.Context(), c.GetString(usernameKey), req.Code)
	if err != nil {
		if errors.Is(err, service.ErrTotpInvalid) || errors.Is(err, service.ErrTotpNotSetup) {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		respondError(c, err)
		return
	}
	h.respondFreshToken(c)
}

// totpDisable 停用两步验证;需当前验证码或登录密码任一确认。
//
// @Summary  停用两步验证
// @Tags     认证
// @Param    body body object true "{password 或 code 任一确认}"
// @Success  204 "已停用"
// @Security BearerAuth
// @Router   /api/v1/auth/totp/disable [post]
func (h *authxHandler) totpDisable(c *gin.Context) {
	var req struct {
		Password string `json:"password"`
		Code     string `json:"code"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	err := h.auth.DisableTotp(c.Request.Context(), c.GetString(usernameKey), req.Password, req.Code)
	if err != nil {
		if errors.Is(err, service.ErrTotpConfirm) {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		respondError(c, err)
		return
	}
	h.respondFreshToken(c)
}

// respondFreshToken 敏感变更后为操作者签发新令牌返回(版本已递增,
// 旧令牌全部失效);签发失败降级 204,前端按会话失效走重新登录。
func (h *authxHandler) respondFreshToken(c *gin.Context) {
	token, expires, err := h.auth.IssueToken(c.Request.Context(), c.GetString(usernameKey))
	if err != nil {
		c.Status(http.StatusNoContent)
		return
	}
	c.JSON(http.StatusOK, gin.H{"token": token, "expiresAt": expires})
}

// revokeSessions 撤销全部会话(令牌版本递增),并为当前操作者重签新令牌。
//
// @Summary  撤销全部会话
// @Tags     认证
// @Success  200 {object} tokenResponse "新 token 与 expiresAt"
// @Security BearerAuth
// @Router   /api/v1/auth/revoke-sessions [post]
func (h *authxHandler) revokeSessions(c *gin.Context) {
	token, expires, err := h.auth.RevokeSessions(c.Request.Context(), c.GetString(usernameKey))
	if err != nil {
		respondError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{"token": token, "expiresAt": expires})
}

// ---- 登录凭据(JWT 组内) ----

// getCredentials 返回登录凭据摘要:当前用户名与密码登录禁用态。
//
// @Summary  登录凭据摘要
// @Tags     认证
// @Success  200 {object} credentialsResponse "username 与 passwordLoginDisabled"
// @Security BearerAuth
// @Router   /api/v1/auth/credentials [get]
func (h *authxHandler) getCredentials(c *gin.Context) {
	disabled, err := h.auth.PasswordLoginDisabled(c.Request.Context())
	if err != nil {
		respondError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"username":              c.GetString(usernameKey),
		"passwordLoginDisabled": disabled,
	})
}

// updateCredentials 修改用户名 / 密码;当前密码必验,改名后旧 JWT 随即失效(前端应重新登录)。
//
// @Summary  修改登录凭据
// @Tags     认证
// @Param    body body service.UpdateCredentialsInput true "新凭据(当前密码必验)"
// @Success  204 "已更新,请重新登录"
// @Failure  401 {object} errorResponse "当前密码错误"
// @Security BearerAuth
// @Router   /api/v1/auth/credentials [put]
func (h *authxHandler) updateCredentials(c *gin.Context) {
	var req service.UpdateCredentialsInput
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	finalName, err := h.auth.UpdateCredentials(c.Request.Context(), c.GetString(usernameKey), req)
	if errors.Is(err, service.ErrCredentialConfirm) {
		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
		return
	}
	if errors.Is(err, service.ErrCredentialInvalid) {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if err != nil {
		respondError(c, err)
		return
	}
	c.Set(usernameKey, finalName)
	h.respondFreshToken(c)
}

// updatePasswordLogin 保存密码登录禁用开关;开启需至少绑定一个外部身份。
//
// @Summary  密码登录开关
// @Tags     认证
// @Param    body body object true "{disabled: bool}"
// @Success  204 "已保存"
// @Failure  409 {object} errorResponse "未绑定外部身份"
// @Security BearerAuth
// @Router   /api/v1/auth/password-login [put]
func (h *authxHandler) updatePasswordLogin(c *gin.Context) {
	var req struct {
		Disabled *bool `json:"disabled" binding:"required"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	err := h.auth.SetPasswordLoginDisabled(c.Request.Context(), c.GetString(usernameKey), *req.Disabled)
	if errors.Is(err, service.ErrNeedIdentity) {
		c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
		return
	}
	if err != nil {
		respondError(c, err)
		return
	}
	h.respondFreshToken(c)
}

// ---- OAuth ----

// oauthProviders 返回已配置的 provider 列表(公开,登录页展示按钮),
// 并附带密码登录禁用开关:开启且存在可用外部身份时,登录页隐藏密码表单;
// provider 清空时不下发禁用,保证界面始终留有登录入口(后端 Login 仍会拒绝)。
//
// @Summary  外部登录 provider 列表
// @Tags     认证
// @Success  200 {object} oauthProvidersResponse "providers 与 passwordLoginDisabled"
// @Router   /api/v1/auth/oauth/providers [get]
func (h *authxHandler) oauthProviders(c *gin.Context) {
	providers := h.oauth.Providers(c.Request.Context())
	disabled := false
	if off, err := h.auth.PasswordLoginDisabled(c.Request.Context()); err == nil && off && len(providers) > 0 {
		disabled = true
	}
	c.JSON(http.StatusOK, gin.H{"providers": providers, "passwordLoginDisabled": disabled})
}

// oauthAuthorize 返回授权跳转 URL;mode=bind 需有效 JWT(绑定到当前账号),login 公开。
//
// @Summary  外部登录授权跳转
// @Tags     认证
// @Param    provider path string true "oidc / github"
// @Param    mode query string false "bind=绑定当前账号(需 Bearer),缺省登录"
// @Success  200 {object} urlResponse "url"
// @Router   /api/v1/auth/oauth/{provider}/authorize [get]
func (h *authxHandler) oauthAuthorize(c *gin.Context) {
	provider := c.Param("provider")
	if provider != "oidc" && provider != "github" {
		c.JSON(http.StatusNotFound, gin.H{"error": "unknown provider"})
		return
	}
	mode, username := "login", ""
	if c.Query("mode") == "bind" {
		name, ok := h.bearerUser(c)
		if !ok {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "绑定需要先登录"})
			return
		}
		mode, username = "bind", name
	}
	authURL, err := h.oauth.AuthorizeURL(c.Request.Context(), provider, mode, username)
	if err != nil {
		if errors.Is(err, service.ErrOAuthNotConfigured) || errors.Is(err, service.ErrOAuthNoAppURL) || errors.Is(err, service.ErrOAuthDisabled) {
			c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
			return
		}
		respondError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{"url": authURL})
}

// bearerUser 从 Authorization 头解析用户名(authorize 在公开组,绑定模式手动校验)。
func (h *authxHandler) bearerUser(c *gin.Context) (string, bool) {
	header := c.GetHeader("Authorization")
	if len(header) < 8 || header[:7] != "Bearer " {
		return "", false
	}
	username, err := h.auth.ParseToken(c.Request.Context(), header[7:])
	if err != nil {
		return "", false
	}
	return username, true
}

// oauthCallback 是 provider 回调落点(浏览器直达):完成登录或绑定后 302 回前端。
//
// @Summary  外部登录回调
// @Tags     认证
// @Param    provider path string true "oidc / github"
// @Param    state query string true "授权流程 state"
// @Param    code query string true "授权码"
// @Success  302 "登录 token 经 fragment 回前端,绑定回设置页"
// @Router   /api/v1/auth/oauth/{provider}/callback [get]
func (h *authxHandler) oauthCallback(c *gin.Context) {
	start := time.Now()
	provider := c.Param("provider")
	token, username, mode, err := h.oauth.HandleCallback(
		c.Request.Context(), provider, c.Query("state"), c.Query("code"))
	if err != nil {
		h.recordOauth(c, username, http.StatusUnauthorized, oauthErrText(err), start)
		target := "/login"
		if mode == "bind" {
			target = "/settings"
		}
		c.Redirect(http.StatusFound, target+"?oauthError="+url.QueryEscape(oauthErrText(err)))
		return
	}
	h.recordOauth(c, username, http.StatusOK, "", start)
	if mode == "bind" {
		// 绑定模式:版本已递增,新 token 经 fragment 带回设置页无感换发
		c.Redirect(http.StatusFound, "/settings?oauth=bound#oauthToken="+url.QueryEscape(token))
		return
	}
	// 登录模式:token 放 fragment,不进服务端日志与 Referer
	c.Redirect(http.StatusFound, "/login#oauthToken="+url.QueryEscape(token))
}

// recordOauth 把外部登录 / 绑定结果记入系统日志——回调是 GET,
// 不经写方法中间件;实际响应恒为 302,日志按语义记 200 / 401。
func (h *authxHandler) recordOauth(c *gin.Context, username string, status int, errMsg string, start time.Time) {
	if h.logs == nil {
		return
	}
	h.logs.Record(model.SystemLog{
		Username:   username,
		Method:     c.Request.Method,
		Path:       requestPath(c),
		Status:     status,
		DurationMs: time.Since(start).Milliseconds(),
		ClientIP:   requestIP(c),
		UserAgent:  truncateLogField(c.Request.UserAgent(), 256),
		ErrMsg:     errMsg,
	})
}

// oauthErrText 把流程错误转为用户可读文案;内部错误不透出细节。
func oauthErrText(err error) string {
	for _, known := range []error{service.ErrOAuthNotConfigured, service.ErrOAuthNoAppURL, service.ErrOAuthDisabled, service.ErrOAuthState, service.ErrOAuthNotBound, service.ErrOAuthBound} {
		if errors.Is(err, known) {
			return known.Error()
		}
	}
	return "登录失败,请重试"
}

// identities 列出当前账号绑定的外部身份。
//
// @Summary  已绑定外部身份
// @Tags     认证
// @Success  200 {object} itemsResponse[model.UserIdentity] "items"
// @Security BearerAuth
// @Router   /api/v1/auth/identities [get]
func (h *authxHandler) identities(c *gin.Context) {
	items, err := h.oauth.Identities(c.Request.Context(), c.GetString(usernameKey))
	if err != nil {
		respondError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{"items": items})
}

// unbindIdentity 解绑外部身份;密码登录禁用期间不允许解绑最后一个身份。
//
// @Summary  解绑外部身份
// @Tags     认证
// @Param    id path int true "身份 ID"
// @Success  204 "已解绑"
// @Failure  409 {object} errorResponse "最后一个身份不可解绑"
// @Security BearerAuth
// @Router   /api/v1/auth/identities/{id} [delete]
func (h *authxHandler) unbindIdentity(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
		return
	}
	if err := h.oauth.Unbind(c.Request.Context(), c.GetString(usernameKey), uint(id)); err != nil {
		if errors.Is(err, service.ErrLastIdentity) {
			c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
			return
		}
		respondError(c, err)
		return
	}
	h.respondFreshToken(c)
}

// ---- OAuth provider 设置(JWT 组内) ----

// getOAuthSettings 返回 provider 配置(secret 只回设置态)。
//
// @Summary  OAuth provider 配置
// @Tags     设置
// @Success  200 {object} service.OAuthProvidersView
// @Security BearerAuth
// @Router   /api/v1/settings/oauth [get]
func (h *authxHandler) getOAuthSettings(c *gin.Context, settings *service.SettingService) {
	view, err := settings.OAuthView(c.Request.Context())
	if err != nil {
		respondError(c, err)
		return
	}
	c.JSON(http.StatusOK, view)
}

// updateOAuthSettings 保存 provider 配置;secret 缺省沿用、空串清除。
//
// @Summary  保存 OAuth provider 配置
// @Tags     设置
// @Param    body body service.UpdateOAuthInput true "provider 配置"
// @Success  200 {object} service.OAuthProvidersView
// @Security BearerAuth
// @Router   /api/v1/settings/oauth [put]
func (h *authxHandler) updateOAuthSettings(c *gin.Context, settings *service.SettingService) {
	var req service.UpdateOAuthInput
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if err := settings.UpdateOAuth(c.Request.Context(), req); err != nil {
		respondError(c, err)
		return
	}
	h.getOAuthSettings(c, settings)
}
