package service

import (
	"context"
	"net/http"
	"strings"
	"time"

	"cmr-backend/internal/apperr"
	"cmr-backend/internal/platform/jwtx"
	"cmr-backend/internal/platform/security"
	"cmr-backend/internal/store/postgres"
)

type OpsAuthSettings struct {
	AppEnv          string
	RefreshTTL      time.Duration
	SMSCodeTTL      time.Duration
	SMSCodeCooldown time.Duration
	SMSProvider     string
	DevSMSCode      string
}

type OpsAuthService struct {
	cfg        OpsAuthSettings
	store      *postgres.Store
	jwtManager *jwtx.Manager
}

type OpsSendSMSCodeInput struct {
	CountryCode string `json:"countryCode"`
	Mobile      string `json:"mobile"`
	DeviceKey   string `json:"deviceKey"`
	Scene       string `json:"scene"`
}

type OpsRegisterInput struct {
	CountryCode string `json:"countryCode"`
	Mobile      string `json:"mobile"`
	Code        string `json:"code"`
	DeviceKey   string `json:"deviceKey"`
	DisplayName string `json:"displayName"`
}

type OpsLoginSMSInput struct {
	CountryCode string `json:"countryCode"`
	Mobile      string `json:"mobile"`
	Code        string `json:"code"`
	DeviceKey   string `json:"deviceKey"`
}

type OpsRefreshTokenInput struct {
	RefreshToken string `json:"refreshToken"`
	DeviceKey    string `json:"deviceKey"`
}

type OpsLogoutInput struct {
	RefreshToken string `json:"refreshToken"`
}

type OpsAuthUser struct {
	ID          string `json:"id"`
	PublicID    string `json:"publicId"`
	DisplayName string `json:"displayName"`
	Status      string `json:"status"`
	RoleCode    string `json:"roleCode"`
}

type OpsAuthResult struct {
	User         OpsAuthUser `json:"user"`
	Tokens       AuthTokens  `json:"tokens"`
	NewUser      bool        `json:"newUser"`
	DevLoginBypass bool      `json:"devLoginBypass,omitempty"`
}

func NewOpsAuthService(cfg OpsAuthSettings, store *postgres.Store, jwtManager *jwtx.Manager) *OpsAuthService {
	return &OpsAuthService{cfg: cfg, store: store, jwtManager: jwtManager}
}

func (s *OpsAuthService) SendSMSCode(ctx context.Context, input OpsSendSMSCodeInput) (*SendSMSCodeResult, error) {
	input.CountryCode = normalizeCountryCode(input.CountryCode)
	input.Mobile = normalizeMobile(input.Mobile)
	input.Scene = normalizeOpsScene(input.Scene)
	if input.Mobile == "" || strings.TrimSpace(input.DeviceKey) == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "mobile and deviceKey are required")
	}

	latest, err := s.store.GetLatestSMSCodeMeta(ctx, input.CountryCode, input.Mobile, "ops", input.Scene)
	if err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	if latest != nil && latest.CooldownUntil.After(now) {
		return nil, apperr.New(http.StatusTooManyRequests, "sms_cooldown", "sms code sent too frequently")
	}

	code := s.cfg.DevSMSCode
	if code == "" {
		code, err = security.GenerateNumericCode(6)
		if err != nil {
			return nil, err
		}
	}
	expiresAt := now.Add(s.cfg.SMSCodeTTL)
	cooldownUntil := now.Add(s.cfg.SMSCodeCooldown)
	if err := s.store.CreateSMSCode(ctx, postgres.CreateSMSCodeParams{
		Scene:         input.Scene,
		CountryCode:   input.CountryCode,
		Mobile:        input.Mobile,
		ClientType:    "ops",
		DeviceKey:     input.DeviceKey,
		CodeHash:      security.HashText(code),
		ProviderName:  s.cfg.SMSProvider,
		ProviderDebug: map[string]any{"mode": s.cfg.SMSProvider, "channel": "ops_console"},
		ExpiresAt:     expiresAt,
		CooldownUntil: cooldownUntil,
	}); err != nil {
		return nil, err
	}

	result := &SendSMSCodeResult{
		TTLSeconds:      int64(s.cfg.SMSCodeTTL.Seconds()),
		CooldownSeconds: int64(s.cfg.SMSCodeCooldown.Seconds()),
	}
	if strings.EqualFold(s.cfg.SMSProvider, "console") || strings.EqualFold(s.cfg.AppEnv, "development") {
		result.DevCode = &code
	}
	return result, nil
}

func (s *OpsAuthService) Register(ctx context.Context, input OpsRegisterInput) (*OpsAuthResult, error) {
	input.CountryCode = normalizeCountryCode(input.CountryCode)
	input.Mobile = normalizeMobile(input.Mobile)
	input.Code = strings.TrimSpace(input.Code)
	input.DeviceKey = strings.TrimSpace(input.DeviceKey)
	input.DisplayName = strings.TrimSpace(input.DisplayName)
	if input.Mobile == "" || input.Code == "" || input.DeviceKey == "" || input.DisplayName == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "mobile, code, deviceKey and displayName are required")
	}

	codeRecord, err := s.store.GetLatestValidSMSCode(ctx, input.CountryCode, input.Mobile, "ops", "ops_register")
	if err != nil {
		return nil, err
	}
	if codeRecord == nil || codeRecord.CodeHash != security.HashText(input.Code) {
		return nil, apperr.New(http.StatusUnauthorized, "invalid_sms_code", "invalid sms code")
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	consumed, err := s.store.ConsumeSMSCode(ctx, tx, codeRecord.ID)
	if err != nil {
		return nil, err
	}
	if !consumed {
		return nil, apperr.New(http.StatusUnauthorized, "invalid_sms_code", "sms code already used")
	}

	existing, err := s.store.GetOpsUserByMobile(ctx, tx, input.CountryCode, input.Mobile)
	if err != nil {
		return nil, err
	}
	if existing != nil {
		return nil, apperr.New(http.StatusConflict, "ops_user_exists", "ops user already exists")
	}

	publicID, err := security.GeneratePublicID("ops")
	if err != nil {
		return nil, err
	}
	user, err := s.store.CreateOpsUser(ctx, tx, postgres.CreateOpsUserParams{
		PublicID:    publicID,
		CountryCode: input.CountryCode,
		Mobile:      input.Mobile,
		DisplayName: input.DisplayName,
		Status:      "active",
	})
	if err != nil {
		return nil, err
	}

	roleCode := "operator"
	count, err := s.store.CountOpsUsers(ctx)
	if err == nil && count == 0 {
		roleCode = "owner"
	}
	role, err := s.store.GetOpsRoleByCode(ctx, tx, roleCode)
	if err != nil {
		return nil, err
	}
	if role == nil {
		return nil, apperr.New(http.StatusInternalServerError, "ops_role_missing", "default ops role is missing")
	}
	if err := s.store.AssignOpsRole(ctx, tx, user.ID, role.ID); err != nil {
		return nil, err
	}
	if err := s.store.TouchOpsUserLogin(ctx, tx, user.ID); err != nil {
		return nil, err
	}

	result, _, err := s.issueAuthResult(ctx, tx, *user, input.DeviceKey, true)
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	return result, nil
}

func (s *OpsAuthService) LoginSMS(ctx context.Context, input OpsLoginSMSInput) (*OpsAuthResult, error) {
	input.CountryCode = normalizeCountryCode(input.CountryCode)
	input.Mobile = normalizeMobile(input.Mobile)
	input.Code = strings.TrimSpace(input.Code)
	input.DeviceKey = strings.TrimSpace(input.DeviceKey)
	if input.Mobile == "" || input.Code == "" || input.DeviceKey == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "mobile, code and deviceKey are required")
	}

	codeRecord, err := s.store.GetLatestValidSMSCode(ctx, input.CountryCode, input.Mobile, "ops", "ops_login")
	if err != nil {
		return nil, err
	}
	if codeRecord == nil || codeRecord.CodeHash != security.HashText(input.Code) {
		return nil, apperr.New(http.StatusUnauthorized, "invalid_sms_code", "invalid sms code")
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	consumed, err := s.store.ConsumeSMSCode(ctx, tx, codeRecord.ID)
	if err != nil {
		return nil, err
	}
	if !consumed {
		return nil, apperr.New(http.StatusUnauthorized, "invalid_sms_code", "sms code already used")
	}

	user, err := s.store.GetOpsUserByMobile(ctx, tx, input.CountryCode, input.Mobile)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return nil, apperr.New(http.StatusNotFound, "ops_user_not_found", "ops user not found")
	}
	if user.Status != "active" {
		return nil, apperr.New(http.StatusForbidden, "ops_user_inactive", "ops user is not active")
	}
	if err := s.store.TouchOpsUserLogin(ctx, tx, user.ID); err != nil {
		return nil, err
	}
	result, _, err := s.issueAuthResult(ctx, tx, *user, input.DeviceKey, false)
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	return result, nil
}

func (s *OpsAuthService) Refresh(ctx context.Context, input OpsRefreshTokenInput) (*OpsAuthResult, error) {
	input.RefreshToken = strings.TrimSpace(input.RefreshToken)
	if input.RefreshToken == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "refreshToken is required")
	}
	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	record, err := s.store.GetOpsRefreshTokenForUpdate(ctx, tx, security.HashText(input.RefreshToken))
	if err != nil {
		return nil, err
	}
	if record == nil || record.IsRevoked || record.ExpiresAt.Before(time.Now().UTC()) {
		return nil, apperr.New(http.StatusUnauthorized, "invalid_refresh_token", "refresh token is invalid or expired")
	}
	if input.DeviceKey != "" && record.DeviceKey != nil && input.DeviceKey != *record.DeviceKey {
		return nil, apperr.New(http.StatusUnauthorized, "invalid_refresh_token", "refresh token device mismatch")
	}
	user, err := s.store.GetOpsUserByID(ctx, tx, record.OpsUserID)
	if err != nil {
		return nil, err
	}
	if user == nil || user.Status != "active" {
		return nil, apperr.New(http.StatusUnauthorized, "invalid_refresh_token", "refresh token user not found")
	}
	result, newTokenID, err := s.issueAuthResult(ctx, tx, *user, nullableStringValue(record.DeviceKey), false)
	if err != nil {
		return nil, err
	}
	if err := s.store.RotateOpsRefreshToken(ctx, tx, record.ID, newTokenID); err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	return result, nil
}

func (s *OpsAuthService) Logout(ctx context.Context, input OpsLogoutInput) error {
	if strings.TrimSpace(input.RefreshToken) == "" {
		return nil
	}
	return s.store.RevokeOpsRefreshToken(ctx, security.HashText(strings.TrimSpace(input.RefreshToken)))
}

func (s *OpsAuthService) GetMe(ctx context.Context, opsUserID string) (*OpsAuthUser, error) {
	user, err := s.store.GetOpsUserByID(ctx, s.store.Pool(), opsUserID)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return nil, apperr.New(http.StatusNotFound, "ops_user_not_found", "ops user not found")
	}
	role, err := s.store.GetPrimaryOpsRole(ctx, s.store.Pool(), user.ID)
	if err != nil {
		return nil, err
	}
	result := buildOpsAuthUser(*user, role)
	return &result, nil
}

func (s *OpsAuthService) issueAuthResult(ctx context.Context, tx postgres.Tx, user postgres.OpsUser, deviceKey string, newUser bool) (*OpsAuthResult, string, error) {
	role, err := s.store.GetPrimaryOpsRole(ctx, tx, user.ID)
	if err != nil {
		return nil, "", err
	}
	roleCode := ""
	if role != nil {
		roleCode = role.RoleCode
	}
	accessToken, accessExpiresAt, err := s.jwtManager.IssueActorAccessToken(user.ID, user.PublicID, "ops", roleCode)
	if err != nil {
		return nil, "", err
	}
	refreshToken, err := security.GenerateToken(32)
	if err != nil {
		return nil, "", err
	}
	refreshTokenHash := security.HashText(refreshToken)
	refreshExpiresAt := time.Now().UTC().Add(s.cfg.RefreshTTL)
	refreshID, err := s.store.CreateOpsRefreshToken(ctx, tx, postgres.CreateOpsRefreshTokenParams{
		OpsUserID: user.ID,
		DeviceKey: deviceKey,
		TokenHash: refreshTokenHash,
		ExpiresAt: refreshExpiresAt,
	})
	if err != nil {
		return nil, "", err
	}
	result := &OpsAuthResult{
		User: buildOpsAuthUser(user, role),
		Tokens: AuthTokens{
			AccessToken:           accessToken,
			AccessTokenExpiresAt:  accessExpiresAt.Format(time.RFC3339),
			RefreshToken:          refreshToken,
			RefreshTokenExpiresAt: refreshExpiresAt.Format(time.RFC3339),
		},
		NewUser: newUser,
	}
	return result, refreshID, nil
}

func buildOpsAuthUser(user postgres.OpsUser, role *postgres.OpsRole) OpsAuthUser {
	roleCode := ""
	if role != nil {
		roleCode = role.RoleCode
	}
	return OpsAuthUser{
		ID:          user.ID,
		PublicID:    user.PublicID,
		DisplayName: user.DisplayName,
		Status:      user.Status,
		RoleCode:    roleCode,
	}
}

func normalizeOpsScene(value string) string {
	switch strings.TrimSpace(value) {
	case "ops_register":
		return "ops_register"
	default:
		return "ops_login"
	}
}
