package usecase

import (
	"context"
	"errors"
	"time"

	member "gateway/internal/model/member/domain"
	"gateway/internal/model/member/domain/entity"
	"gateway/internal/model/member/domain/enum"
	domrepo "gateway/internal/model/member/domain/repository"
	domusecase "gateway/internal/model/member/domain/usecase"
)

type provisioningUseCase struct {
	members    domrepo.MemberRepository
	identities domrepo.IdentityRepository
	tenants    domrepo.TenantRepository
	uidGen     domrepo.UIDGenerator
}

// ProvisioningUseCaseParam wires ProvisioningUseCase.
type ProvisioningUseCaseParam struct {
	Members    domrepo.MemberRepository
	Identities domrepo.IdentityRepository
	Tenants    domrepo.TenantRepository
	UIDGen     domrepo.UIDGenerator
}

// MustProvisioningUseCase constructs ProvisioningUseCase.
func MustProvisioningUseCase(param ProvisioningUseCaseParam) domusecase.ProvisioningUseCase {
	if param.Members == nil || param.Identities == nil || param.Tenants == nil || param.UIDGen == nil {
		panic("member: provisioning dependencies are required")
	}
	return &provisioningUseCase{
		members:    param.Members,
		identities: param.Identities,
		tenants:    param.Tenants,
		uidGen:     param.UIDGen,
	}
}

func (uc *provisioningUseCase) EnsureFromOIDC(ctx context.Context, req *domusecase.EnsureFromOIDCRequest) (*domusecase.MemberDTO, error) {
	if req == nil || req.TenantID == "" || req.ZitadelSub == "" {
		return nil, errb.InputMissingRequired("tenant_id and zitadel_sub are required")
	}
	if existing, err := uc.members.GetByZitadelUserID(ctx, req.TenantID, req.ZitadelSub); err == nil {
		return memberToDTO(existing), nil
	} else if !errors.Is(err, member.ErrNotFound) {
		return nil, wrapRepoErr(err, "read member failed")
	}
	return uc.createProvisioned(ctx, req.TenantID, req.ZitadelSub, "", req.Email, req.DisplayName, req.Locale, enum.MemberOriginOIDC)
}

func (uc *provisioningUseCase) EnsureFromLDAP(ctx context.Context, req *domusecase.EnsureFromLDAPRequest) (*domusecase.MemberDTO, error) {
	if req == nil || req.TenantID == "" {
		return nil, errb.InputMissingRequired("tenant_id is required")
	}
	sub := req.ZitadelSub
	if sub == "" {
		sub = req.ExternalID
	}
	if sub != "" {
		if existing, err := uc.members.GetByZitadelUserID(ctx, req.TenantID, sub); err == nil {
			return memberToDTO(existing), nil
		} else if !errors.Is(err, member.ErrNotFound) {
			return nil, wrapRepoErr(err, "read member failed")
		}
	}
	if req.ExternalID != "" {
		if idn, err := uc.identities.GetByExternalID(ctx, req.TenantID, req.ExternalID); err == nil {
			rec, getErr := uc.members.GetByUID(ctx, req.TenantID, idn.UID)
			if getErr == nil {
				return memberToDTO(rec), nil
			}
		}
	}
	return uc.createProvisioned(ctx, req.TenantID, sub, req.ExternalID, req.Email, req.DisplayName, "", enum.MemberOriginLDAP)
}

func (uc *provisioningUseCase) EnsureFromSCIM(ctx context.Context, req *domusecase.EnsureFromSCIMRequest) (*domusecase.MemberDTO, error) {
	if req == nil || req.TenantID == "" {
		return nil, errb.InputMissingRequired("tenant_id is required")
	}
	if req.ExternalID != "" {
		if idn, err := uc.identities.GetByExternalID(ctx, req.TenantID, req.ExternalID); err == nil {
			rec, getErr := uc.members.GetByUID(ctx, req.TenantID, idn.UID)
			if getErr == nil {
				return memberToDTO(rec), nil
			}
		}
	}
	sub := req.ZitadelSub
	return uc.createProvisioned(ctx, req.TenantID, sub, req.ExternalID, req.Email, req.DisplayName, "", enum.MemberOriginSCIM)
}

func (uc *provisioningUseCase) createProvisioned(
	ctx context.Context,
	tenantID, zitadelSub, externalID, email, displayName, language string,
	origin enum.MemberOrigin,
) (*domusecase.MemberDTO, error) {
	tenant, err := uc.tenants.GetByTenantID(ctx, tenantID)
	if err != nil {
		if errors.Is(err, member.ErrTenantNotFound) {
			return nil, errb.ResNotFound("tenant", tenantID).WithCause(err)
		}
		return nil, wrapRepoErr(err, "read tenant failed")
	}
	uid, err := uc.uidGen.Next(ctx, tenantID, tenant.UIDPrefix)
	if err != nil {
		return nil, wrapRepoErr(err, "allocate uid failed")
	}
	now := time.Now().UTC().UnixMilli()
	rec := &entity.Member{
		TenantID:      tenantID,
		UID:           uid,
		ZitadelUserID: zitadelSub,
		ZitadelEmail:  email,
		DisplayName:   displayName,
		Language:      defaultLanguage(language),
		Status:        enum.MemberStatusActive,
		Origin:        origin,
		CreateAt:      now,
		UpdateAt:      now,
	}
	if err := uc.members.Insert(ctx, rec); err != nil {
		if errors.Is(err, member.ErrDuplicateMember) {
			if zitadelSub != "" {
				if existing, getErr := uc.members.GetByZitadelUserID(ctx, tenantID, zitadelSub); getErr == nil {
					return memberToDTO(existing), nil
				}
			}
			return nil, errb.ResAlreadyExist("member already exists").WithCause(err)
		}
		return nil, wrapRepoErr(err, "create member failed")
	}
	idn := &entity.Identity{
		TenantID:      tenantID,
		UID:           uid,
		ZitadelUserID: zitadelSub,
		ExternalID:    externalID,
		CreateAt:      now,
		UpdateAt:      now,
	}
	if err := uc.identities.Insert(ctx, idn); err != nil && !errors.Is(err, member.ErrDuplicateMember) {
		return nil, wrapRepoErr(err, "create identity failed")
	}
	return memberToDTO(rec), nil
}
