// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package authcrunch

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"

	"github.com/greenpau/go-authcrunch/pkg/authn"
	"github.com/greenpau/go-authcrunch/pkg/authz"
	"github.com/greenpau/go-authcrunch/pkg/credentials"
	"github.com/greenpau/go-authcrunch/pkg/errors"
	"github.com/greenpau/go-authcrunch/pkg/idp"
	"github.com/greenpau/go-authcrunch/pkg/ids"
	"github.com/greenpau/go-authcrunch/pkg/messaging"
	"github.com/greenpau/go-authcrunch/pkg/registry"
	"github.com/greenpau/go-authcrunch/pkg/sso"
)

// Config is a configuration of Server.
type Config struct {
	Credentials               *credentials.Config               `json:"credentials,omitempty" xml:"credentials,omitempty" yaml:"credentials,omitempty"`
	Messaging                 *messaging.Config                 `json:"messaging,omitempty" xml:"messaging,omitempty" yaml:"messaging,omitempty"`
	UserRegistration          *registry.Config                  `json:"user_registration,omitempty" xml:"user_registration,omitempty" yaml:"user_registration,omitempty"`
	AuthenticationPortals     []*authn.PortalConfig             `json:"authentication_portals,omitempty" xml:"authentication_portals,omitempty" yaml:"authentication_portals,omitempty"`
	AuthorizationPolicies     []*authz.PolicyConfig             `json:"authorization_policies,omitempty" xml:"authorization_policies,omitempty" yaml:"authorization_policies,omitempty"`
	IdentityStores            []*ids.IdentityStoreConfig        `json:"identity_stores,omitempty" xml:"identity_stores,omitempty" yaml:"identity_stores,omitempty"`
	IdentityProviders         []*idp.IdentityProviderConfig     `json:"identity_providers,omitempty" xml:"identity_providers,omitempty" yaml:"identity_providers,omitempty"`
	SingleSignOnProviders     []*sso.SingleSignOnProviderConfig `json:"sso_providers,omitempty" xml:"sso_providers,omitempty" yaml:"sso_providers,omitempty"`
	disabledIdentityStores    map[string]interface{}
	disabledIdentityProviders map[string]interface{}
}

// NewConfig returns an instance of Config.
func NewConfig() *Config {
	return &Config{}
}

// AddCredential adds a credential configuration.
func (cfg *Config) AddCredential(instructions []string) {
	if cfg.Credentials == nil {
		cfg.Credentials = &credentials.Config{}
	}
	cfg.Credentials.Add(instructions)
}

// AddMessagingProvider adds a messaging provider configuration.
func (cfg *Config) AddMessagingProvider(instructions []string) {
	if cfg.Messaging == nil {
		cfg.Messaging = &messaging.Config{}
	}
	cfg.Messaging.Add(instructions)
}

// AddIdentityStore adds an identity store configuration.
func (cfg *Config) AddIdentityStore(name, kind string, data map[string]interface{}) error {
	store, err := ids.NewIdentityStoreConfig(name, kind, data)
	if err != nil {
		return err
	}
	cfg.IdentityStores = append(cfg.IdentityStores, store)
	return nil
}

// AddIdentityProvider adds an identity provider configuration.
func (cfg *Config) AddIdentityProvider(name, kind string, data map[string]interface{}) error {
	provider, err := idp.NewIdentityProviderConfig(name, kind, data)
	if err != nil {
		return err
	}
	cfg.IdentityProviders = append(cfg.IdentityProviders, provider)
	return nil
}

// AddSingleSignOnProvider adds a single sign-on provider configuration.
func (cfg *Config) AddSingleSignOnProvider(data map[string]interface{}) error {
	provider, err := sso.NewSingleSignOnProviderConfig(data)
	if err != nil {
		return err
	}
	cfg.SingleSignOnProviders = append(cfg.SingleSignOnProviders, provider)
	return nil
}

// AddAuthenticationPortal adds an authentication portal configuration.
func (cfg *Config) AddAuthenticationPortal(p *authn.PortalConfig) error {
	if err := p.Validate(); err != nil {
		return err
	}
	cfg.AuthenticationPortals = append(cfg.AuthenticationPortals, p)
	return nil
}

// AddAuthorizationPolicy adds an authorization policy configuration.
func (cfg *Config) AddAuthorizationPolicy(p *authz.PolicyConfig) error {
	if err := p.Validate(); err != nil {
		return err
	}
	cfg.AuthorizationPolicies = append(cfg.AuthorizationPolicies, p)
	return nil
}

// Validate validates Config.
func (cfg *Config) Validate() error {
	if cfg == nil {
		return fmt.Errorf("config is nil")
	}
	if cfg.Credentials == nil {
		cfg.Credentials = &credentials.Config{}
	}

	if len(cfg.Credentials.RawCredentialConfigs) > 0 {
		if err := cfg.Credentials.Validate(); err != nil {
			return err
		}
	}

	if cfg.UserRegistration == nil {
		cfg.UserRegistration = &registry.Config{}
	}

	if len(cfg.UserRegistration.RawConfigs) > 0 {
		if err := cfg.UserRegistration.Validate(); err != nil {
			return err
		}
	}

	if len(cfg.AuthenticationPortals) < 1 && len(cfg.AuthorizationPolicies) < 1 {
		return fmt.Errorf("no portals and gatekeepers found")
	}

	identityStoreUserRegistry := make(map[string]string)
	for _, userRegistry := range cfg.UserRegistration.GetProviders() {
		if err := userRegistry.SetCredentials(cfg.Credentials); err != nil {
			return err
		}
		if err := userRegistry.SetMessaging(cfg.Messaging); err != nil {
			return err
		}

		var identityStoreFound bool
		for _, identityStore := range cfg.IdentityStores {
			if identityStore.Name == userRegistry.GetIdentityStoreName() {
				identityStoreFound = true
				identityStoreUserRegistry[identityStore.Name] = userRegistry.GetIdentityStoreName()
				break
			}
		}
		if !identityStoreFound {
			return fmt.Errorf(
				"identity store %q referenced in %q user registry not found",
				userRegistry.GetIdentityStoreName(), userRegistry.GetName(),
			)
		}
	}

	// Validate auth portal configurations.
	for _, portalCfg := range cfg.AuthenticationPortals {
		// If there are no excplicitly specified identity stores and providers in a portal, add all of them.
		if len(portalCfg.IdentityStores) == 0 && len(portalCfg.IdentityProviders) == 0 {
			for _, entry := range cfg.IdentityStores {
				portalCfg.IdentityStores = append(portalCfg.IdentityStores, entry.Name)
			}
			for _, entry := range cfg.IdentityProviders {
				portalCfg.IdentityProviders = append(portalCfg.IdentityProviders, entry.Name)
			}
		}

		if len(portalCfg.IdentityStores) == 0 && len(portalCfg.IdentityProviders) == 0 {
			return errors.ErrPortalConfigBackendsNotFound
		}

		// Filter out disabled identity store names.
		portalCfg.IdentityStores = cfg.filterDisabledIdentityStores(portalCfg.IdentityStores)

		// Vealidate that there are no duplicate or overlapping identity store and providers.
		authByName := make(map[string]string)
		authByRealm := make(map[string]string)

		for _, storeName := range portalCfg.IdentityStores {
			if v, exists := authByName[storeName]; exists {
				return fmt.Errorf(
					"identity store %q has the same name as %s",
					storeName, v,
				)
			}

			authByName[storeName] = "another identity store"

			var storeConfig *ids.IdentityStoreConfig
			for _, entry := range cfg.IdentityStores {
				if entry.Name == storeName {
					storeConfig = entry
					break
				}
			}
			if storeConfig == nil {
				continue
			}
			if storeConfig.Params == nil {
				continue
			}
			if v, exists := storeConfig.Params["realm"]; exists {
				realmName := v.(string)
				if prevStoreName, exists := authByRealm[realmName]; exists {
					return fmt.Errorf(
						"identity provider %q has the same %q realm as %q",
						storeName, realmName, prevStoreName,
					)
				}
				authByRealm[realmName] = storeName
				authByName[storeName] = "identity store in " + realmName + " realm"
			}

			// Add registry store if configured.
			if v, exists := identityStoreUserRegistry[storeName]; exists {
				storeConfig.Params["registration_enabled"] = true
				portalCfg.UserRegistries = append(portalCfg.UserRegistries, v)
			}
		}

		// Filter out disabled identity store names.
		portalCfg.IdentityProviders = cfg.filterDisabledIdentityProviders(portalCfg.IdentityProviders)

		for _, providerName := range portalCfg.IdentityProviders {
			if v, exists := authByName[providerName]; exists {
				return fmt.Errorf(
					"identity provider %q has the same name as %s",
					providerName, v,
				)
			}

			authByName[providerName] = "another identity provider"

			var providerConfig *idp.IdentityProviderConfig
			for _, entry := range cfg.IdentityProviders {
				providerConfig = entry
				if entry.Name == providerName {
					break
				}
			}
			if providerConfig == nil {
				continue
			}
			if providerConfig.Params == nil {
				continue
			}
			if v, exists := providerConfig.Params["realm"]; exists {
				realmName := v.(string)
				if prevProviderName, exists := authByRealm[realmName]; exists {
					return fmt.Errorf(
						"identity provider %q has the same %q realm as %q",
						providerName, realmName, prevProviderName,
					)
				}
				authByRealm[realmName] = providerName
				authByName[providerName] = "identity provider in " + realmName + " realm"
			}
		}

		// Iterate over SSO providers.
		for _, providerName := range portalCfg.SingleSignOnProviders {
			var providerFound bool
			for _, entry := range cfg.SingleSignOnProviders {
				if providerName == entry.Name {
					providerFound = true
					break
				}
			}
			if !providerFound {
				return fmt.Errorf("sso provider %q configuration not found", providerName)
			}
		}
	}

	return nil
}

// AddDisabledIdentityStore adds the names of disabled identity stores.
func (cfg *Config) AddDisabledIdentityStore(s string) {
	if cfg.disabledIdentityStores == nil {
		cfg.disabledIdentityStores = map[string]interface{}{
			s: true,
		}
		return
	}
	cfg.disabledIdentityStores[s] = true
}

// AddDisabledIdentityProvider adds the names of disabled identity providers.
func (cfg *Config) AddDisabledIdentityProvider(s string) {
	if cfg.disabledIdentityProviders == nil {
		cfg.disabledIdentityProviders = map[string]interface{}{
			s: true,
		}
		return
	}
	cfg.disabledIdentityProviders[s] = true
}

func (cfg *Config) filterDisabledIdentityStores(arr []string) []string {
	var output []string
	if len(arr) == 0 || cfg.disabledIdentityStores == nil {
		return arr
	}
	for _, s := range arr {
		if _, exists := cfg.disabledIdentityStores[s]; exists {
			continue
		}
		output = append(output, s)
	}
	return output
}

func (cfg *Config) filterDisabledIdentityProviders(arr []string) []string {
	var output []string
	if len(arr) == 0 || cfg.disabledIdentityProviders == nil {
		return arr
	}
	for _, s := range arr {
		if _, exists := cfg.disabledIdentityProviders[s]; exists {
			continue
		}
		output = append(output, s)
	}
	return output
}

// AddUserRegistry adds a user registry configuration.
func (cfg *Config) AddUserRegistry(instructions []string) {
	if cfg.UserRegistration == nil {
		cfg.UserRegistration = &registry.Config{}
	}
	cfg.UserRegistration.RawConfigs = append(cfg.UserRegistration.RawConfigs, instructions)
}

// LoadFromJSONFile loads configuration from a JSON file.
func (cfg *Config) LoadFromJSONFile(filePath string) error {
	data, err := os.ReadFile(filePath)
	if err != nil {
		return err
	}
	return json.Unmarshal(data, cfg)
}

// DumpToJSONFile stores configuration in a JSON file with pretty-printed formatting.
func (cfg *Config) DumpToJSONFile(filePath string) error {
	rawBytes, err := json.Marshal(cfg)
	if err != nil {
		return fmt.Errorf("failed to marshal config: %w", err)
	}

	var prettyBuf bytes.Buffer
	err = json.Indent(&prettyBuf, rawBytes, "", "\t")
	if err != nil {
		return fmt.Errorf("error indenting JSON: %w", err)
	}

	if err := os.WriteFile(filePath, prettyBuf.Bytes(), 0644); err != nil {
		return fmt.Errorf("failed to write config file: %w", err)
	}

	return nil
}
