package command

import (
	"context"
	"time"

	"github.com/zitadel/zitadel/internal/api/authz"
	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/repository/samlrequest"
	"github.com/zitadel/zitadel/internal/telemetry/tracing"
	"github.com/zitadel/zitadel/internal/zerrors"
)

type SAMLRequest struct {
	ID          string
	LoginClient string

	ApplicationID string
	ACSURL        string
	RelayState    string
	RequestID     string
	Binding       string
	Issuer        string
	Destination   string
}

type CurrentSAMLRequest struct {
	*SAMLRequest
	SessionID   string
	UserID      string
	AuthMethods []domain.UserAuthMethodType
	AuthTime    time.Time
}

func (c *Commands) AddSAMLRequest(ctx context.Context, samlRequest *SAMLRequest) (_ *CurrentSAMLRequest, err error) {
	id, err := c.idGenerator.Next()
	if err != nil {
		return nil, err
	}
	samlRequest.ID = IDPrefixV2 + id
	writeModel, err := c.getSAMLRequestWriteModel(ctx, samlRequest.ID)
	if err != nil {
		return nil, err
	}
	if writeModel.SAMLRequestState != domain.SAMLRequestStateUnspecified {
		return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-MO3vmsMLUt", "Errors.SAMLRequest.AlreadyExisting")
	}
	err = c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewAddedEvent(
		ctx,
		&samlrequest.NewAggregate(samlRequest.ID, authz.GetInstance(ctx).InstanceID()).Aggregate,
		samlRequest.LoginClient,
		samlRequest.ApplicationID,
		samlRequest.ACSURL,
		samlRequest.RelayState,
		samlRequest.RequestID,
		samlRequest.Binding,
		samlRequest.Issuer,
		samlRequest.Destination,
	))
	if err != nil {
		return nil, err
	}
	return samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil
}

func (c *Commands) LinkSessionToSAMLRequest(ctx context.Context, id, sessionID, sessionToken string, checkLoginClient bool) (*domain.ObjectDetails, *CurrentSAMLRequest, error) {
	writeModel, err := c.getSAMLRequestWriteModel(ctx, id)
	if err != nil {
		return nil, nil, err
	}
	if writeModel.SAMLRequestState == domain.SAMLRequestStateUnspecified {
		return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-GH3PVLSfXC", "Errors.SAMLRequest.NotExisting")
	}
	if writeModel.SAMLRequestState != domain.SAMLRequestStateAdded {
		return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-ttPKNdAIFT", "Errors.SAMLRequest.AlreadyHandled")
	}
	if checkLoginClient && authz.GetCtxData(ctx).UserID != writeModel.LoginClient {
		return nil, nil, zerrors.ThrowPermissionDenied(nil, "COMMAND-KCd48Rxt7x", "Errors.SAMLRequest.WrongLoginClient")
	}
	sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID())
	err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
	if err != nil {
		return nil, nil, err
	}
	if err = sessionWriteModel.CheckIsActive(); err != nil {
		return nil, nil, err
	}
	if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil {
		return nil, nil, err
	}

	if err := c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewSessionLinkedEvent(
		ctx, &samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
		sessionID,
		sessionWriteModel.UserID,
		sessionWriteModel.AuthenticationTime(),
		sessionWriteModel.AuthMethodTypes(),
	)); err != nil {
		return nil, nil, err
	}
	return writeModelToObjectDetails(&writeModel.WriteModel), samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil
}

func (c *Commands) FailSAMLRequest(ctx context.Context, id string, reason domain.SAMLErrorReason) (*domain.ObjectDetails, *CurrentSAMLRequest, error) {
	writeModel, err := c.getSAMLRequestWriteModel(ctx, id)
	if err != nil {
		return nil, nil, err
	}
	if writeModel.SAMLRequestState != domain.SAMLRequestStateAdded {
		return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-32lGj1Fhjt", "Errors.SAMLRequest.AlreadyHandled")
	}
	err = c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewFailedEvent(
		ctx,
		&samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
		reason,
	))
	if err != nil {
		return nil, nil, err
	}
	return writeModelToObjectDetails(&writeModel.WriteModel), samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil
}

func samlRequestWriteModelToCurrentSAMLRequest(writeModel *SAMLRequestWriteModel) (_ *CurrentSAMLRequest) {
	return &CurrentSAMLRequest{
		SAMLRequest: &SAMLRequest{
			ID:            writeModel.AggregateID,
			LoginClient:   writeModel.LoginClient,
			ApplicationID: writeModel.ApplicationID,
			ACSURL:        writeModel.ACSURL,
			RelayState:    writeModel.RelayState,
			RequestID:     writeModel.RequestID,
			Binding:       writeModel.Binding,
			Issuer:        writeModel.Issuer,
			Destination:   writeModel.Destination,
		},
		SessionID:   writeModel.SessionID,
		UserID:      writeModel.UserID,
		AuthMethods: writeModel.AuthMethods,
		AuthTime:    writeModel.AuthTime,
	}
}

func (c *Commands) GetCurrentSAMLRequest(ctx context.Context, id string) (_ *CurrentSAMLRequest, err error) {
	wm, err := c.getSAMLRequestWriteModel(ctx, id)
	if err != nil {
		return nil, err
	}
	return samlRequestWriteModelToCurrentSAMLRequest(wm), nil
}

func (c *Commands) getSAMLRequestWriteModel(ctx context.Context, id string) (writeModel *SAMLRequestWriteModel, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	writeModel = NewSAMLRequestWriteModel(ctx, id)
	err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
	if err != nil {
		return nil, err
	}
	return writeModel, nil
}