package auth

import (
	"context"
	"errors"
	"strings"

	errs "gateway/internal/library/errors"
	"gateway/internal/library/zitadel"
	authmetaenum "gateway/internal/model/auth/domain/enum"
	domauth "gateway/internal/model/auth/domain/usecase"
	memberenum "gateway/internal/model/member/domain/enum"
	dommember "gateway/internal/model/member/domain/usecase"
	"gateway/internal/svc"
	"gateway/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

func resolveTenant(ctx context.Context, sc *svc.ServiceContext, slug string) (*dommember.TenantDTO, error) {
	if sc.MemberTenant == nil {
		return nil, errb.SysNotImplemented("member tenant not configured")
	}
	slug = strings.TrimSpace(slug)
	tenant, err := sc.MemberTenant.ResolveBySlug(ctx, slug)
	if err != nil {
		return nil, err
	}
	if tenant.Status != memberenum.TenantStatusActive.String() {
		return nil, errb.AuthForbidden("tenant registration is not allowed")
	}
	return tenant, nil
}

func wrapZitadelErr(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, zitadel.ErrNotConfigured) {
		return errb.SysNotImplemented("zitadel not configured").WithCause(err)
	}
	if errors.Is(err, zitadel.ErrUserAlreadyExists) {
		return errb.ResAlreadyExist("email already registered").WithCause(err)
	}
	if errors.Is(err, zitadel.ErrInvalidCredentials) {
		return errb.AuthUnauthorized("invalid credentials").WithCause(err)
	}
	if errors.Is(err, zitadel.ErrInvalidIDToken) {
		return errb.AuthUnauthorized("invalid id_token").WithCause(err)
	}
	if e := errs.FromError(err); e != nil {
		return err
	}
	return errb.SvcThirdParty("zitadel request failed").WithCause(err)
}

func registrationPurpose() memberenum.OTPPurpose {
	return memberenum.OTPPurposeRegistrationEmail
}

func recordRegistrationMeta(
	ctx context.Context,
	sc *svc.ServiceContext,
	tenantID, uid, inviteCodeID, acceptTermsVersion string,
	marketingOptIn bool,
	channel authmetaenum.RegistrationChannel,
) error {
	if sc.AuthRegistrationMeta == nil {
		return errb.SysNotImplemented("registration metadata not configured")
	}
	meta := RequestMetaFromContext(ctx)
	return sc.AuthRegistrationMeta.Record(ctx, &domauth.RecordRegistrationRequest{
		TenantID:           tenantID,
		UID:                uid,
		InviteCodeID:       inviteCodeID,
		AcceptTermsVersion: acceptTermsVersion,
		MarketingOptIn:     marketingOptIn,
		Channel:            channel,
		ClientIP:           strings.TrimSpace(meta.ClientIP),
		UserAgent:          strings.TrimSpace(meta.UserAgent),
	})
}

func requireRegistrationDeps(sc *svc.ServiceContext) error {
	if sc.Zitadel == nil {
		return errb.SysNotImplemented("zitadel not configured")
	}
	if sc.MemberLifecycle == nil {
		return errb.SysNotImplemented("member lifecycle not configured")
	}
	if sc.MemberProfile == nil {
		return errb.SysNotImplemented("member profile not configured")
	}
	if sc.MemberOTP == nil {
		return errb.SysNotImplemented("member OTP not configured")
	}
	if sc.MemberVerifyRate == nil {
		return errb.SysNotImplemented("member verify rate not configured")
	}
	if sc.Notifier == nil {
		return errb.SysNotImplemented("notifier not configured")
	}
	return nil
}

func resumeRegistration(
	ctx context.Context,
	sc *svc.ServiceContext,
	tenantSlug, email string,
) (*types.RegisterData, error) {
	tenant, err := resolveTenant(ctx, sc, tenantSlug)
	if err != nil {
		return nil, err
	}

	email = normalizeLoginEmail(email)
	member, err := sc.MemberProfile.GetByZitadelEmail(ctx, tenant.TenantID, email)
	if err != nil {
		if isMemberNotFound(err) {
			return nil, errb.ResNotFound("member", email)
		}
		return nil, err
	}
	if member.Status != memberenum.MemberStatusUnverified {
		return nil, errb.ResInvalidState("account already verified, please login")
	}

	data, err := sendRegistrationOTP(ctx, sc, tenant.TenantID, member.UID, email)
	if err != nil {
		return nil, err
	}
	data.UID = member.UID
	return data, nil
}

func recoverPendingRegistration(
	ctx context.Context,
	sc *svc.ServiceContext,
	tenant *dommember.TenantDTO,
	req *types.RegisterReq,
) (*types.RegisterData, error) {
	if req == nil {
		return nil, errb.InputMissingRequired("request body is required")
	}

	email := normalizeLoginEmail(req.Email)
	tok, err := sc.Zitadel.VerifyPassword(ctx, email, req.Password)
	if err != nil {
		return nil, errb.AuthUnauthorized("invalid credentials").WithCause(wrapZitadelErr(err))
	}

	identity, err := zitadelIdentityFromToken(ctx, sc.Zitadel, tok)
	if err != nil {
		return nil, err
	}

	memberDTO, err := memberForRegistrationRecovery(ctx, sc, tenant.TenantID, identity.Sub, email, req)
	if err != nil {
		return nil, err
	}

	switch memberDTO.Status {
	case memberenum.MemberStatusUnverified:
	case memberenum.MemberStatusActive:
		return nil, errb.ResAlreadyExist("email already registered, please login")
	default:
		return nil, errb.ResInvalidState("account cannot complete registration")
	}

	data, err := sendRegistrationOTP(ctx, sc, tenant.TenantID, memberDTO.UID, email)
	if err != nil {
		return nil, err
	}
	data.UID = memberDTO.UID
	return data, nil
}

func memberForRegistrationRecovery(
	ctx context.Context,
	sc *svc.ServiceContext,
	tenantID, zitadelSub, email string,
	req *types.RegisterReq,
) (*dommember.MemberDTO, error) {
	if dto, err := sc.MemberProfile.GetByZitadelUserID(ctx, tenantID, zitadelSub); err == nil {
		return dto, nil
	} else if !isMemberNotFound(err) {
		return nil, err
	}

	if dto, err := sc.MemberProfile.GetByZitadelEmail(ctx, tenantID, email); err == nil {
		return dto, nil
	} else if !isMemberNotFound(err) {
		return nil, err
	}

	memberDTO, err := sc.MemberLifecycle.CreateUnverified(ctx, &dommember.CreatePlatformMemberRequest{
		TenantID:      tenantID,
		Email:         email,
		DisplayName:   strings.TrimSpace(req.DisplayName),
		Language:      strings.TrimSpace(req.Language),
		ZitadelUserID: zitadelSub,
	})
	if err != nil {
		return nil, err
	}
	if err := recordRegistrationMeta(ctx, sc, tenantID, memberDTO.UID, "", req.AcceptTermsVersion, req.MarketingOptIn, authmetaenum.RegistrationChannelEmail); err != nil {
		logx.WithContext(ctx).Infof("register recover: registration meta skipped: %v", err)
	}
	return memberDTO, nil
}
