package security

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"slices"

	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/datetime"
	"golang.org/x/oauth2"
	"gopkg.in/yaml.v3"
)

const (
	AuthMethodBasic  = "basic"
	AuthMethodApiKey = "apikey"
	AuthMethodCas    = "cas"
	AuthMethodSaml   = "saml"
	AuthMethodLdap   = "ldap"
	AuthMethodOAuth2 = "oauth2"
)

const DefaultInactivityInterval = 24 // hours

const configPath = "/api/security/config.yml"

// Config providers which auth methods must be used.
type Config struct {
	Security struct {
		AuthProviders []string     `yaml:"auth_providers"`
		Basic         BasicConfig  `yaml:"basic"`
		Ldap          LdapConfig   `yaml:"ldap"`
		Cas           CasConfig    `yaml:"cas"`
		Saml          SamlConfig   `yaml:"saml"`
		OAuth2        OAuth2Config `yaml:"oauth2"`
	} `yaml:"security"`
}

func (c *Config) GetIdpFieldsExtraRolesAllowed(source string) ([]string, bool, bool) {
	if source == "" {
		return nil, false, false
	}

	var idpAttributesMap map[string]string
	var allowExtraRoles bool

	switch source {
	case SourceSaml:
		if !slices.Contains(c.Security.AuthProviders, SourceSaml) {
			return nil, false, false
		}

		idpAttributesMap = c.Security.Saml.IdPAttributesMap
		allowExtraRoles = c.Security.Saml.AllowExtraRoles
	case SourceLdap:
		if !slices.Contains(c.Security.AuthProviders, SourceLdap) {
			return nil, false, false
		}

		idpAttributesMap = c.Security.Ldap.Attributes
		allowExtraRoles = true
	default:
		if !slices.Contains(c.Security.AuthProviders, SourceOauth2) {
			return nil, false, false
		}

		cfg, ok := c.Security.OAuth2.Providers[source]
		if !ok {
			return nil, false, false
		}

		idpAttributesMap = cfg.AttributesMap
		allowExtraRoles = cfg.AllowExtraRoles
	}

	idpFields := make([]string, 0, len(idpAttributesMap))
	for k := range idpAttributesMap {
		idpFields = append(idpFields, k)
	}

	return idpFields, allowExtraRoles, true
}

type BasicConfig struct {
	InactivityInterval string `yaml:"inactivity_interval"`
	ExpirationInterval string `yaml:"expiration_interval"`
}

type LdapConfig struct {
	InactivityInterval    string            `yaml:"inactivity_interval"`
	ExpirationInterval    string            `yaml:"expiration_interval"`
	Url                   string            `yaml:"url"`
	AdminUsername         string            `yaml:"admin_dn"`
	AdminPassword         string            `yaml:"admin_passwd"`
	BaseDN                string            `yaml:"user_dn"`
	Attributes            map[string]string `yaml:"attrs"`
	UsernameAttr          string            `yaml:"username_attr"`
	Filter                string            `yaml:"ufilter"`
	DefaultRole           string            `yaml:"default_role"`
	InsecureSkipVerify    bool              `yaml:"insecure_skip_verify"`
	InsecureVerifyAnyCert bool              `yaml:"insecure_verify_any_cert"`
	MinTLSVersion         string            `yaml:"min_tls_ver"`
	MaxTLSVersion         string            `yaml:"max_tls_ver"`
}

type CasConfig struct {
	InactivityInterval    string `yaml:"inactivity_interval"`
	ExpirationInterval    string `yaml:"expiration_interval"`
	Title                 string `yaml:"title"`
	LoginUrl              string `yaml:"login_url"`
	ValidateUrl           string `yaml:"validate_url"`
	DefaultRole           string `yaml:"default_role"`
	InsecureSkipVerify    bool   `yaml:"insecure_skip_verify"`
	InsecureVerifyAnyCert bool   `yaml:"insecure_verify_any_cert"`
}

type SamlConfig struct {
	InactivityInterval      string            `yaml:"inactivity_interval"`
	ExpirationInterval      string            `yaml:"expiration_interval"`
	Title                   string            `yaml:"title"`
	X509Cert                string            `yaml:"x509_cert"`
	X509Key                 string            `yaml:"x509_key"`
	IdPMetadataUrl          string            `yaml:"idp_metadata_url"`
	IdPMetadataXml          string            `yaml:"idp_metadata_xml"`
	IdPAttributesMap        map[string]string `yaml:"idp_attributes_map"`
	CanopsisSamlUrl         string            `yaml:"canopsis_saml_url"`
	DefaultRole             string            `yaml:"default_role"`
	InsecureSkipVerify      bool              `yaml:"insecure_skip_verify"`
	InsecureVerifyAnyCert   bool              `yaml:"insecure_verify_any_cert"`
	CanopsisSSOBinding      string            `yaml:"canopsis_sso_binding"`
	CanopsisACSBinding      string            `yaml:"canopsis_acs_binding"`
	SignAuthRequest         bool              `yaml:"sign_auth_request"`
	NameIdFormat            string            `yaml:"name_id_format"`
	SkipSignatureValidation bool              `yaml:"skip_signature_validation"`
	ACSIndex                *int              `yaml:"acs_index"`
	AutoUserRegistration    bool              `yaml:"auto_user_registration"`
	AllowExtraRoles         bool              `yaml:"allow_extra_roles"`
}

type OAuth2Config struct {
	Providers map[string]OAuth2ProviderConfig `yaml:"providers"`
}

type OAuth2ProviderConfig struct {
	InactivityInterval    string            `yaml:"inactivity_interval"`
	ExpirationInterval    string            `yaml:"expiration_interval"`
	Issuer                string            `yaml:"issuer"`
	ClientID              string            `yaml:"client_id"`
	ClientSecret          string            `yaml:"client_secret"`
	RedirectURL           string            `yaml:"redirect_url"`
	DefaultRole           string            `yaml:"default_role"`
	AllowExtraRoles       bool              `yaml:"allow_extra_roles"`
	AuthURL               string            `yaml:"auth_url"`
	TokenURL              string            `yaml:"token_url"`
	UserURL               string            `yaml:"user_url"`
	UserID                string            `yaml:"user_id"`
	Scopes                []string          `yaml:"scopes"`
	AttributesMap         map[string]string `yaml:"attributes_map"`
	OpenID                bool              `yaml:"open_id"`
	PKCE                  bool              `yaml:"pkce"`
	InsecureSkipVerify    bool              `yaml:"insecure_skip_verify"`
	InsecureVerifyAnyCert bool              `yaml:"insecure_verify_any_cert"`
	AuthStyle             oauth2.AuthStyle  `yaml:"auth_style"`
}

// LoadConfig creates Config by config file.
func LoadConfig(configDir string) (Config, error) {
	buf, err := os.ReadFile(filepath.Join(configDir, configPath))
	if err != nil {
		return Config{}, err
	}

	var config Config
	err = yaml.Unmarshal(buf, &config)
	if err != nil {
		return Config{}, err
	}

	err = validateConfig(config)
	if err != nil {
		return Config{}, err
	}

	return config, nil
}

func validateOAuth2Config(config OAuth2ProviderConfig) error {
	if config.ExpirationInterval != "" {
		_, err := datetime.ParseDurationWithUnit(config.ExpirationInterval)
		if err != nil {
			return fmt.Errorf("invalid expiration_interval: %w", err)
		}
	}

	if config.InactivityInterval != "" {
		_, err := datetime.ParseDurationWithUnit(config.InactivityInterval)
		if err != nil {
			return fmt.Errorf("invalid inactivity_interval: %w", err)
		}
	}

	if config.OpenID {
		if config.Issuer == "" {
			return errors.New("issuer shouldn't be empty")
		}
	} else {
		if config.UserID == "" {
			return errors.New("user_id shouldn't be empty")
		}

		if config.TokenURL == "" {
			return errors.New("token_url shouldn't be empty")
		}

		if config.AuthURL == "" {
			return errors.New("auth_url shouldn't be empty")
		}

		if config.UserURL == "" {
			return errors.New("user_url shouldn't be empty")
		}
	}

	if config.RedirectURL == "" {
		return errors.New("redirect_url shouldn't be empty")
	}

	if config.ClientID == "" {
		return errors.New("client_id shouldn't be empty")
	}

	if config.ClientSecret == "" {
		return errors.New("client_secret shouldn't be empty")
	}

	switch config.AuthStyle {
	case oauth2.AuthStyleAutoDetect, oauth2.AuthStyleInParams, oauth2.AuthStyleInHeader:
	default:
		return fmt.Errorf("auth style should be one of: %d, %d, %d", oauth2.AuthStyleAutoDetect, oauth2.AuthStyleInParams, oauth2.AuthStyleInHeader)
	}

	return nil
}

func validateConfig(config Config) error {
	for _, name := range config.Security.AuthProviders {
		switch name {
		case AuthMethodApiKey:
		case AuthMethodBasic:
			if config.Security.Basic.ExpirationInterval != "" {
				_, err := datetime.ParseDurationWithUnit(config.Security.Basic.ExpirationInterval)
				if err != nil {
					return fmt.Errorf("invalid basic.expiration_interval: %w", err)
				}
			}

			if config.Security.Basic.InactivityInterval != "" {
				_, err := datetime.ParseDurationWithUnit(config.Security.Basic.InactivityInterval)
				if err != nil {
					return fmt.Errorf("invalid basic.inactivity_interval: %w", err)
				}
			}
		case AuthMethodLdap:
			if config.Security.Ldap.ExpirationInterval != "" {
				_, err := datetime.ParseDurationWithUnit(config.Security.Ldap.ExpirationInterval)
				if err != nil {
					return fmt.Errorf("invalid ldap.expiration_interval: %w", err)
				}
			}

			if config.Security.Ldap.InactivityInterval != "" {
				_, err := datetime.ParseDurationWithUnit(config.Security.Ldap.InactivityInterval)
				if err != nil {
					return fmt.Errorf("invalid ldap.inactivity_interval: %w", err)
				}
			}

			if config.Security.Ldap.DefaultRole == "" {
				return errors.New("ldap.default_role is required")
			}
		case AuthMethodCas:
			if config.Security.Cas.ExpirationInterval != "" {
				_, err := datetime.ParseDurationWithUnit(config.Security.Cas.ExpirationInterval)
				if err != nil {
					return fmt.Errorf("invalid cas.expiration_interval: %w", err)
				}
			}

			if config.Security.Cas.InactivityInterval != "" {
				_, err := datetime.ParseDurationWithUnit(config.Security.Cas.InactivityInterval)
				if err != nil {
					return fmt.Errorf("invalid cas.inactivity_interval: %w", err)
				}
			}

			if config.Security.Cas.DefaultRole == "" {
				return errors.New("cas.default_role is required")
			}
		case AuthMethodSaml:
			if config.Security.Saml.ExpirationInterval != "" {
				_, err := datetime.ParseDurationWithUnit(config.Security.Saml.ExpirationInterval)
				if err != nil {
					return fmt.Errorf("invalid saml.expiration_interval: %w", err)
				}
			}

			if config.Security.Saml.InactivityInterval != "" {
				_, err := datetime.ParseDurationWithUnit(config.Security.Saml.InactivityInterval)
				if err != nil {
					return fmt.Errorf("invalid saml.inactivity_interval: %w", err)
				}
			}
		case AuthMethodOAuth2:
			for provider, cfg := range config.Security.OAuth2.Providers {
				if err := validateOAuth2Config(cfg); err != nil {
					return fmt.Errorf("invalid %s provider config: %w", provider, err)
				}
			}
		default:
			panic("unknown auth provider: " + name)
		}
	}

	return nil
}
