package command

import (
	"strings"

	"github.com/zitadel/zitadel/internal/command/preparation"
	"github.com/zitadel/zitadel/internal/eventstore"
	"github.com/zitadel/zitadel/internal/repository/instance"
	"github.com/zitadel/zitadel/internal/repository/project"
)

type SystemConfigWriteModel struct {
	eventstore.WriteModel

	Instances         map[string]*systemConfigChangesInstanceModel
	externalDomain    string
	externalPort      uint16
	externalSecure    bool
	newExternalDomain string
	newExternalPort   uint16
	newExternalSecure bool
}

type systemConfigChangesInstanceModel struct {
	Domains                []string
	GeneratedDomain        string
	ProjectID              string
	ConsoleAppID           string
	RedirectUris           []string
	PostLogoutRedirectUris []string
}

func NewSystemConfigWriteModel(externalDomain, newExternalDomain string, externalPort, newExternalPort uint16, externalSecure, newExternalSecure bool) *SystemConfigWriteModel {
	return &SystemConfigWriteModel{
		WriteModel:        eventstore.WriteModel{},
		Instances:         make(map[string]*systemConfigChangesInstanceModel),
		externalDomain:    externalDomain,
		externalPort:      externalPort,
		externalSecure:    externalSecure,
		newExternalDomain: newExternalDomain,
		newExternalPort:   newExternalPort,
		newExternalSecure: newExternalSecure,
	}
}

func (wm *SystemConfigWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *instance.InstanceAddedEvent:
			wm.Instances[e.Aggregate().InstanceID] = &systemConfigChangesInstanceModel{}
		case *instance.InstanceRemovedEvent:
			delete(wm.Instances, e.Aggregate().InstanceID)
		case *instance.DomainAddedEvent:
			if !e.Generated && e.Domain != wm.externalDomain && e.Domain != wm.newExternalDomain {
				continue
			}
			if e.Generated && !strings.HasSuffix(e.Domain, wm.externalDomain) && !strings.HasSuffix(e.Domain, wm.newExternalDomain) {
				continue
			}
			wm.Instances[e.Aggregate().InstanceID].Domains = append(wm.Instances[e.Aggregate().InstanceID].Domains, e.Domain)
		case *instance.DomainRemovedEvent:
			instance, ok := wm.Instances[e.Aggregate().InstanceID]
			if !ok {
				continue
			}

			for i, domain := range instance.Domains {
				if domain == e.Domain {
					instance.Domains[i] = instance.Domains[len(instance.Domains)-1]
					instance.Domains[len(instance.Domains)-1] = ""
					wm.Instances[e.Aggregate().InstanceID].Domains = instance.Domains[:len(instance.Domains)-1]
					break
				}
			}
		case *instance.ProjectSetEvent:
			wm.Instances[e.Aggregate().InstanceID].ProjectID = e.ProjectID
		case *instance.ConsoleSetEvent:
			wm.Instances[e.Aggregate().InstanceID].ConsoleAppID = e.AppID
		case *project.OIDCConfigAddedEvent:
			if wm.Instances[e.Aggregate().InstanceID].ConsoleAppID != e.AppID {
				continue
			}
			wm.Instances[e.Aggregate().InstanceID].RedirectUris = e.RedirectUris
			wm.Instances[e.Aggregate().InstanceID].PostLogoutRedirectUris = e.PostLogoutRedirectUris
		case *project.OIDCConfigChangedEvent:
			if wm.Instances[e.Aggregate().InstanceID].ConsoleAppID != e.AppID {
				continue
			}
			if e.RedirectUris != nil {
				wm.Instances[e.Aggregate().InstanceID].RedirectUris = *e.RedirectUris
			}
			if e.PostLogoutRedirectUris != nil {
				wm.Instances[e.Aggregate().InstanceID].PostLogoutRedirectUris = *e.PostLogoutRedirectUris
			}
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *SystemConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
	return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
		AddQuery().
		AggregateTypes(instance.AggregateType).
		EventTypes(
			instance.InstanceAddedEventType,
			instance.InstanceRemovedEventType,
			instance.InstanceDomainAddedEventType,
			instance.InstanceDomainRemovedEventType,
			instance.ProjectSetEventType,
			instance.ConsoleSetEventType,
		).
		Or().
		AggregateTypes(project.AggregateType).
		EventTypes(
			project.OIDCConfigAddedType,
			project.OIDCConfigChangedType,
		).
		Builder()
}

type SystemConfigChangesValidation struct {
	ProjectID    string
	ConsoleAppID string
	Validations  []preparation.Validation
	InstanceID   string
}

func (wm *SystemConfigWriteModel) NewChangedEvents(commands *Commands) map[string]*SystemConfigChangesValidation {
	var newCustomDomainExists, isInstanceOfCustomDomain bool
	var instanceOfCustomDomain string
	cmds := make(map[string]*SystemConfigChangesValidation)
	for i, inst := range wm.Instances {
		cmds[i] = &SystemConfigChangesValidation{
			InstanceID:   i,
			ProjectID:    inst.ProjectID,
			ConsoleAppID: inst.ConsoleAppID,
		}
		//check each instance separately for changes (using the generated domain) and check if there's an existing custom domain
		newCustomDomainExists, isInstanceOfCustomDomain = wm.changeConfig(cmds[i], inst, commands)
		if isInstanceOfCustomDomain || newCustomDomainExists {
			instanceOfCustomDomain = i
		}
	}
	//handle the custom domain at last
	if newCustomDomainExists {
		//if the domain itself already exists, then only check if the uris of the console app exist as well
		wm.changeURIs(cmds[instanceOfCustomDomain], wm.Instances[instanceOfCustomDomain], commands, wm.newExternalDomain)
		return cmds
	}
	//otherwise the add instance domain will take care of the uris
	cmds[instanceOfCustomDomain].Validations = append(cmds[instanceOfCustomDomain].Validations, commands.addInstanceDomain(instance.NewAggregate(instanceOfCustomDomain), wm.newExternalDomain, false))
	return cmds
}

func (wm *SystemConfigWriteModel) changeConfig(validation *SystemConfigChangesValidation, inst *systemConfigChangesInstanceModel, commands *Commands) (newCustomDomainExists, isInstanceOfCustomDomain bool) {
	var newGeneratedDomain string
	var newGeneratedDomainExists bool
	for _, domain := range inst.Domains {
		if domain == wm.newExternalDomain {
			newCustomDomainExists = true
			continue
		}
		if domain != wm.newExternalDomain && strings.HasSuffix(domain, wm.newExternalDomain) {
			newGeneratedDomainExists = true
		}
		if !newCustomDomainExists && domain == wm.externalDomain {
			isInstanceOfCustomDomain = true
		}
		if domain != wm.externalDomain && strings.HasSuffix(domain, wm.externalDomain) {
			newGeneratedDomain = strings.TrimSuffix(domain, wm.externalDomain) + wm.newExternalDomain
		}
	}
	if newGeneratedDomainExists {
		//if the domain itself already exists, then only check if the uris of the console app exist as well
		wm.changeURIs(validation, inst, commands, newGeneratedDomain)
		return newCustomDomainExists, isInstanceOfCustomDomain
	}
	//otherwise the add instance domain will take care of the uris
	validation.Validations = append(validation.Validations, commands.addInstanceDomain(instance.NewAggregate(validation.InstanceID), newGeneratedDomain, true))
	return newCustomDomainExists, isInstanceOfCustomDomain
}

func (wm *SystemConfigWriteModel) changeURIs(validation *SystemConfigChangesValidation, inst *systemConfigChangesInstanceModel, commands *Commands, domain string) {
	validation.Validations = append(validation.Validations, commands.prepareUpdateConsoleRedirectURIs(domain))
}