package command

import (
	"bytes"
	"context"
	"encoding/json"
	"time"

	"github.com/zitadel/zitadel/internal/api/authz"
	"github.com/zitadel/zitadel/internal/crypto"
	"github.com/zitadel/zitadel/internal/domain"
	domain_schema "github.com/zitadel/zitadel/internal/domain/schema"
	"github.com/zitadel/zitadel/internal/eventstore"
	"github.com/zitadel/zitadel/internal/repository/user/schemauser"
	"github.com/zitadel/zitadel/internal/zerrors"
)

type UserV3WriteModel struct {
	eventstore.WriteModel

	PhoneWM bool
	EmailWM bool
	DataWM  bool

	SchemaID       string
	SchemaRevision uint64

	Email                    string
	IsEmailVerified          bool
	EmailVerifiedFailedCount int
	EmailCode                *VerifyCode

	Phone                    string
	IsPhoneVerified          bool
	PhoneVerifiedFailedCount int
	PhoneCode                *VerifyCode

	Data json.RawMessage

	Locked bool
	State  domain.UserState

	checkPermission      domain.PermissionCheck
	writePermissionCheck bool
}

func (wm *UserV3WriteModel) GetWriteModel() *eventstore.WriteModel {
	return &wm.WriteModel
}

type VerifyCode struct {
	Code         *crypto.CryptoValue
	CreationDate time.Time
	Expiry       time.Duration
}

func NewExistsUserV3WriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
	return &UserV3WriteModel{
		WriteModel: eventstore.WriteModel{
			AggregateID:   userID,
			ResourceOwner: resourceOwner,
		},
		PhoneWM:         false,
		EmailWM:         false,
		DataWM:          false,
		checkPermission: checkPermission,
	}
}

func NewUserV3WriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
	return &UserV3WriteModel{
		WriteModel: eventstore.WriteModel{
			AggregateID:   userID,
			ResourceOwner: resourceOwner,
		},
		PhoneWM:         true,
		EmailWM:         true,
		DataWM:          true,
		checkPermission: checkPermission,
	}
}

func NewUserV3EmailWriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
	return &UserV3WriteModel{
		WriteModel: eventstore.WriteModel{
			AggregateID:   userID,
			ResourceOwner: resourceOwner,
		},
		EmailWM:         true,
		checkPermission: checkPermission,
	}
}

func NewUserV3PhoneWriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
	return &UserV3WriteModel{
		WriteModel: eventstore.WriteModel{
			AggregateID:   userID,
			ResourceOwner: resourceOwner,
		},
		PhoneWM:         true,
		checkPermission: checkPermission,
	}
}

func (wm *UserV3WriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *schemauser.CreatedEvent:
			wm.SchemaID = e.SchemaID
			wm.SchemaRevision = e.SchemaRevision
			wm.Data = e.Data
			wm.Locked = false

			wm.State = domain.UserStateActive
		case *schemauser.UpdatedEvent:
			if e.SchemaID != nil {
				wm.SchemaID = *e.SchemaID
			}
			if e.SchemaRevision != nil {
				wm.SchemaRevision = *e.SchemaRevision
			}
			if len(e.Data) > 0 {
				wm.Data = e.Data
			}
		case *schemauser.DeletedEvent:
			wm.State = domain.UserStateDeleted
		case *schemauser.EmailUpdatedEvent:
			wm.Email = string(e.EmailAddress)
			wm.IsEmailVerified = false
			wm.EmailVerifiedFailedCount = 0
			wm.EmailCode = nil
		case *schemauser.EmailCodeAddedEvent:
			wm.IsEmailVerified = false
			wm.EmailVerifiedFailedCount = 0
			wm.EmailCode = &VerifyCode{
				Code:         e.Code,
				CreationDate: e.CreationDate(),
				Expiry:       e.Expiry,
			}
		case *schemauser.EmailVerifiedEvent:
			wm.IsEmailVerified = true
			wm.EmailVerifiedFailedCount = 0
			wm.EmailCode = nil
		case *schemauser.EmailVerificationFailedEvent:
			wm.EmailVerifiedFailedCount += 1
		case *schemauser.PhoneUpdatedEvent:
			wm.Phone = string(e.PhoneNumber)
			wm.IsPhoneVerified = false
			wm.PhoneVerifiedFailedCount = 0
			wm.EmailCode = nil
		case *schemauser.PhoneCodeAddedEvent:
			wm.IsPhoneVerified = false
			wm.PhoneVerifiedFailedCount = 0
			wm.PhoneCode = &VerifyCode{
				Code:         e.Code,
				CreationDate: e.CreationDate(),
				Expiry:       e.Expiry,
			}
		case *schemauser.PhoneVerifiedEvent:
			wm.PhoneVerifiedFailedCount = 0
			wm.IsPhoneVerified = true
			wm.PhoneCode = nil
		case *schemauser.PhoneVerificationFailedEvent:
			wm.PhoneVerifiedFailedCount += 1
		case *schemauser.LockedEvent:
			wm.Locked = true
		case *schemauser.UnlockedEvent:
			wm.Locked = false
		case *schemauser.DeactivatedEvent:
			wm.State = domain.UserStateInactive
		case *schemauser.ActivatedEvent:
			wm.State = domain.UserStateActive
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *UserV3WriteModel) Query() *eventstore.SearchQueryBuilder {
	builder := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent)
	if wm.ResourceOwner != "" {
		builder = builder.ResourceOwner(wm.ResourceOwner)
	}
	eventtypes := []eventstore.EventType{
		schemauser.CreatedType,
		schemauser.DeletedType,
		schemauser.ActivatedType,
		schemauser.DeactivatedType,
		schemauser.LockedType,
		schemauser.UnlockedType,
	}
	if wm.DataWM {
		eventtypes = append(eventtypes,
			schemauser.UpdatedType,
		)
	}
	if wm.EmailWM {
		eventtypes = append(eventtypes,
			schemauser.EmailUpdatedType,
			schemauser.EmailVerifiedType,
			schemauser.EmailCodeAddedType,
			schemauser.EmailVerificationFailedType,
		)
	}
	if wm.PhoneWM {
		eventtypes = append(eventtypes,
			schemauser.PhoneUpdatedType,
			schemauser.PhoneVerifiedType,
			schemauser.PhoneCodeAddedType,
			schemauser.PhoneVerificationFailedType,
		)
	}
	return builder.AddQuery().
		AggregateTypes(schemauser.AggregateType).
		AggregateIDs(wm.AggregateID).
		EventTypes(eventtypes...).Builder()
}

func (wm *UserV3WriteModel) NewCreated(
	ctx context.Context,
	schemaID string,
	schemaRevision uint64,
	data json.RawMessage,
	email *Email,
	phone *Phone,
	emailCode func(context.Context) (*EncryptedCode, error),
	phoneCode func(context.Context) (*EncryptedCode, string, error),
) (_ []eventstore.Command, codeEmail string, codePhone string, err error) {
	if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
		return nil, "", "", err
	}
	if wm.Exists() {
		return nil, "", "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.AlreadyExists")
	}
	events := []eventstore.Command{
		schemauser.NewCreatedEvent(ctx,
			UserV3AggregateFromWriteModel(&wm.WriteModel),
			schemaID, schemaRevision, data,
		),
	}
	if email != nil {
		emailEvents, plainCodeEmail, err := wm.NewEmailCreate(ctx,
			email,
			emailCode,
		)
		if err != nil {
			return nil, "", "", err
		}
		if plainCodeEmail != "" {
			codeEmail = plainCodeEmail
		}
		events = append(events, emailEvents...)
	}

	if phone != nil {
		phoneEvents, plainCodePhone, err := wm.NewPhoneCreate(ctx,
			phone,
			phoneCode,
		)
		if err != nil {
			return nil, "", "", err
		}
		if plainCodePhone != "" {
			codePhone = plainCodePhone
		}
		events = append(events, phoneEvents...)
	}

	return events, codeEmail, codePhone, nil
}

func (wm *UserV3WriteModel) getSchemaRoleForWrite(ctx context.Context, resourceOwner, userID string) (domain_schema.Role, error) {
	if userID == authz.GetCtxData(ctx).UserID {
		return domain_schema.RoleSelf, nil
	}
	if err := wm.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
		return domain_schema.RoleUnspecified, err
	}
	return domain_schema.RoleOwner, nil
}

func (wm *UserV3WriteModel) validateData(ctx context.Context, data []byte, schemaWM *UserSchemaWriteModel) (string, uint64, error) {
	// get role for permission check in schema through extension
	role, err := wm.getSchemaRoleForWrite(ctx, wm.ResourceOwner, wm.AggregateID)
	if err != nil {
		return "", 0, err
	}

	schema, err := domain_schema.NewSchema(role, bytes.NewReader(schemaWM.Schema))
	if err != nil {
		return "", 0, err
	}

	// if data not changed but a new schema or revision should be used
	if data == nil {
		data = wm.Data
	}
	var v interface{}
	if err := json.Unmarshal(data, &v); err != nil {
		return "", 0, zerrors.ThrowInvalidArgument(nil, "COMMAND-7o3ZGxtXUz", "Errors.User.Invalid")
	}

	if err := schema.Validate(v); err != nil {
		return "", 0, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")
	}
	return schemaWM.AggregateID, schemaWM.SchemaRevision, nil
}

func (wm *UserV3WriteModel) NewUpdate(
	ctx context.Context,
	schemaWM *UserSchemaWriteModel,
	user *SchemaUser,
	email *Email,
	phone *Phone,
	emailCode func(context.Context) (*EncryptedCode, error),
	phoneCode func(context.Context) (*EncryptedCode, string, error),
) (_ []eventstore.Command, codeEmail string, codePhone string, err error) {
	if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
		return nil, "", "", err
	}
	if !wm.Exists() {
		return nil, "", "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.NotFound")
	}
	events := make([]eventstore.Command, 0)
	if user != nil {
		schemaID, schemaRevision, err := wm.validateData(ctx, user.Data, schemaWM)
		if err != nil {
			return nil, "", "", err
		}
		userEvents := wm.newUpdatedEvents(ctx,
			schemaID,
			schemaRevision,
			user.Data,
		)
		events = append(events, userEvents...)
	}
	if email != nil {
		emailEvents, plainCodeEmail, err := wm.NewEmailUpdate(ctx,
			email,
			emailCode,
		)
		if err != nil {
			return nil, "", "", err
		}
		if plainCodeEmail != "" {
			codeEmail = plainCodeEmail
		}
		events = append(events, emailEvents...)
	}

	if phone != nil {
		phoneEvents, plainCodePhone, err := wm.NewPhoneCreate(ctx,
			phone,
			phoneCode,
		)
		if err != nil {
			return nil, "", "", err
		}
		if plainCodePhone != "" {
			codePhone = plainCodePhone
		}
		events = append(events, phoneEvents...)
	}

	return events, codeEmail, codePhone, nil
}

func (wm *UserV3WriteModel) newUpdatedEvents(
	ctx context.Context,
	schemaID string,
	schemaRevision uint64,
	data json.RawMessage,
) []eventstore.Command {
	changes := make([]schemauser.Changes, 0)
	if wm.SchemaID != schemaID {
		changes = append(changes, schemauser.ChangeSchemaID(schemaID))
	}
	if wm.SchemaRevision != schemaRevision {
		changes = append(changes, schemauser.ChangeSchemaRevision(schemaRevision))
	}
	if data != nil && !bytes.Equal(wm.Data, data) {
		changes = append(changes, schemauser.ChangeData(data))
	}
	if len(changes) == 0 {
		return nil
	}
	return []eventstore.Command{schemauser.NewUpdatedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel), changes)}
}

func (wm *UserV3WriteModel) NewDelete(
	ctx context.Context,
) (_ []eventstore.Command, err error) {
	if !wm.Exists() {
		return nil, zerrors.ThrowNotFound(nil, "COMMAND-syHyCsGmvM", "Errors.User.NotFound")
	}
	if err := wm.checkPermissionDelete(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
		return nil, err
	}
	return []eventstore.Command{schemauser.NewDeletedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel))}, nil

}

func UserV3AggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
	return &eventstore.Aggregate{
		ID:            wm.AggregateID,
		Type:          schemauser.AggregateType,
		ResourceOwner: wm.ResourceOwner,
		InstanceID:    wm.InstanceID,
		Version:       schemauser.AggregateVersion,
	}
}

func (wm *UserV3WriteModel) Exists() bool {
	return wm.State != domain.UserStateDeleted && wm.State != domain.UserStateUnspecified
}

func (wm *UserV3WriteModel) checkPermissionWrite(
	ctx context.Context,
	resourceOwner string,
	userID string,
) error {
	if wm.writePermissionCheck {
		return nil
	}
	if userID != "" && userID == authz.GetCtxData(ctx).UserID {
		return nil
	}
	if err := wm.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
		return err
	}
	wm.writePermissionCheck = true
	return nil
}

func (wm *UserV3WriteModel) checkPermissionDelete(
	ctx context.Context,
	resourceOwner string,
	userID string,
) error {
	if userID != "" && userID == authz.GetCtxData(ctx).UserID {
		return nil
	}
	return wm.checkPermission(ctx, domain.PermissionUserDelete, resourceOwner, userID)
}

func (wm *UserV3WriteModel) NewEmailCreate(
	ctx context.Context,
	email *Email,
	code func(context.Context) (*EncryptedCode, error),
) (_ []eventstore.Command, plainCode string, err error) {
	if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
		return nil, "", err
	}
	if email == nil || wm.Email == string(email.Address) {
		return nil, "", nil
	}
	events := []eventstore.Command{
		schemauser.NewEmailUpdatedEvent(ctx,
			UserV3AggregateFromWriteModel(&wm.WriteModel),
			email.Address,
		),
	}
	if email.Verified {
		events = append(events, wm.newEmailVerifiedEvent(ctx))
	} else {
		codeEvent, code, err := wm.newEmailCodeAddedEvent(ctx, code, email.URLTemplate, email.ReturnCode)
		if err != nil {
			return nil, "", err
		}
		events = append(events, codeEvent)
		if code != "" {
			plainCode = code
		}
	}
	return events, plainCode, nil
}

func (wm *UserV3WriteModel) NewEmailUpdate(
	ctx context.Context,
	email *Email,
	code func(context.Context) (*EncryptedCode, error),
) (_ []eventstore.Command, plainCode string, err error) {
	if !wm.EmailWM {
		return nil, "", nil
	}
	if !wm.Exists() {
		return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-nJ0TQFuRmP", "Errors.User.NotFound")
	}
	return wm.NewEmailCreate(ctx, email, code)
}

func (wm *UserV3WriteModel) NewEmailVerify(
	ctx context.Context,
	verify func(creationDate time.Time, expiry time.Duration, cryptoCode *crypto.CryptoValue) error,
) ([]eventstore.Command, error) {
	if !wm.EmailWM {
		return nil, nil
	}
	if !wm.Exists() {
		return nil, zerrors.ThrowNotFound(nil, "COMMAND-qbGyMPvjvj", "Errors.User.NotFound")
	}
	if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
		return nil, err
	}
	if wm.EmailCode == nil {
		return nil, nil
	}
	if err := verify(wm.EmailCode.CreationDate, wm.EmailCode.Expiry, wm.EmailCode.Code); err != nil {
		return nil, err
	}
	return []eventstore.Command{wm.newEmailVerifiedEvent(ctx)}, nil
}

func (wm *UserV3WriteModel) newEmailVerifiedEvent(
	ctx context.Context,
) *schemauser.EmailVerifiedEvent {
	return schemauser.NewEmailVerifiedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel))
}

func (wm *UserV3WriteModel) NewResendEmailCode(
	ctx context.Context,
	code func(context.Context) (*EncryptedCode, error),
	urlTemplate string,
	isReturnCode bool,
) (_ []eventstore.Command, plainCode string, err error) {
	if !wm.EmailWM {
		return nil, "", nil
	}
	if !wm.Exists() {
		return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-EajeF6ypOV", "Errors.User.NotFound")
	}
	if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
		return nil, "", err
	}
	if wm.EmailCode == nil {
		return nil, "", zerrors.ThrowPreconditionFailed(err, "COMMAND-QRkNTBwF8q", "Errors.User.Code.Empty")
	}
	event, plainCode, err := wm.newEmailCodeAddedEvent(ctx, code, urlTemplate, isReturnCode)
	if err != nil {
		return nil, "", err
	}
	return []eventstore.Command{event}, plainCode, nil
}

func (wm *UserV3WriteModel) newEmailCodeAddedEvent(
	ctx context.Context,
	code func(context.Context) (*EncryptedCode, error),
	urlTemplate string,
	isReturnCode bool,
) (_ *schemauser.EmailCodeAddedEvent, plainCode string, err error) {
	cryptoCode, err := code(ctx)
	if err != nil {
		return nil, "", err
	}
	if isReturnCode {
		plainCode = cryptoCode.Plain
	}
	return schemauser.NewEmailCodeAddedEvent(ctx,
		UserV3AggregateFromWriteModel(&wm.WriteModel),
		cryptoCode.Crypted,
		cryptoCode.Expiry,
		urlTemplate,
		isReturnCode,
	), plainCode, nil
}

func (wm *UserV3WriteModel) NewPhoneCreate(
	ctx context.Context,
	phone *Phone,
	code func(context.Context) (*EncryptedCode, string, error),
) (_ []eventstore.Command, plainCode string, err error) {
	if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
		return nil, "", err
	}
	if phone == nil || wm.Phone == string(phone.Number) {
		return nil, "", nil
	}
	events := []eventstore.Command{
		schemauser.NewPhoneUpdatedEvent(ctx,
			UserV3AggregateFromWriteModel(&wm.WriteModel),
			phone.Number,
		),
	}
	if phone.Verified {
		events = append(events, wm.newPhoneVerifiedEvent(ctx))
	} else {
		codeEvent, code, err := wm.newPhoneCodeAddedEvent(ctx, code, phone.ReturnCode)
		if err != nil {
			return nil, "", err
		}
		events = append(events, codeEvent)
		if code != "" {
			plainCode = code
		}
	}
	return events, plainCode, nil
}

func (wm *UserV3WriteModel) NewPhoneUpdate(
	ctx context.Context,
	phone *Phone,
	code func(context.Context) (*EncryptedCode, string, error),
) (_ []eventstore.Command, plainCode string, err error) {
	if !wm.PhoneWM {
		return nil, "", nil
	}
	if !wm.Exists() {
		return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-b33QAVgel6", "Errors.User.NotFound")
	}
	return wm.NewPhoneCreate(ctx, phone, code)
}

func (wm *UserV3WriteModel) NewPhoneVerify(
	ctx context.Context,
	verify func(creationDate time.Time, expiry time.Duration, cryptoCode *crypto.CryptoValue) error,
) ([]eventstore.Command, error) {
	if !wm.PhoneWM {
		return nil, nil
	}
	if !wm.Exists() {
		return nil, zerrors.ThrowNotFound(nil, "COMMAND-bx2OLtgGNS", "Errors.User.NotFound")
	}
	if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
		return nil, err
	}
	if wm.PhoneCode == nil {
		return nil, nil
	}
	if err := verify(wm.PhoneCode.CreationDate, wm.PhoneCode.Expiry, wm.PhoneCode.Code); err != nil {
		return nil, err
	}
	return []eventstore.Command{wm.newPhoneVerifiedEvent(ctx)}, nil
}

func (wm *UserV3WriteModel) newPhoneVerifiedEvent(
	ctx context.Context,
) *schemauser.PhoneVerifiedEvent {
	return schemauser.NewPhoneVerifiedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel))
}

func (wm *UserV3WriteModel) NewResendPhoneCode(
	ctx context.Context,
	code func(context.Context) (*EncryptedCode, string, error),
	isReturnCode bool,
) (_ []eventstore.Command, plainCode string, err error) {
	if !wm.PhoneWM {
		return nil, "", nil
	}
	if !wm.Exists() {
		return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-z8Bu9vuL9s", "Errors.User.NotFound")
	}
	if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
		return nil, "", err
	}
	if wm.PhoneCode == nil {
		return nil, "", zerrors.ThrowPreconditionFailed(err, "COMMAND-fEsHdqECzb", "Errors.User.Code.Empty")
	}
	event, plainCode, err := wm.newPhoneCodeAddedEvent(ctx, code, isReturnCode)
	if err != nil {
		return nil, "", err
	}
	return []eventstore.Command{event}, plainCode, nil
}

func (wm *UserV3WriteModel) newPhoneCodeAddedEvent(
	ctx context.Context,
	code func(context.Context) (*EncryptedCode, string, error),
	isReturnCode bool,
) (_ *schemauser.PhoneCodeAddedEvent, plainCode string, err error) {
	cryptoCode, generatorID, err := code(ctx)
	if err != nil {
		return nil, "", err
	}
	if isReturnCode {
		plainCode = cryptoCode.Plain
	}
	return schemauser.NewPhoneCodeAddedEvent(ctx,
		UserV3AggregateFromWriteModel(&wm.WriteModel),
		cryptoCode.CryptedCode(),
		cryptoCode.CodeExpiry(),
		isReturnCode,
		generatorID,
	), plainCode, nil
}