package integrationservice

import (
	"bytes"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	connectedmailhttp "github.com/pingcap-inc/nexuscrm-connectors/features/connector/connectedmailhttp"
	connectoroutboundintent "github.com/pingcap-inc/nexuscrm-connectors/features/connector/outboundintent"
	catalogv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
)

const (
	ConnectedMailDefinitionFileEnv    = "NEXUSCRM_INTEGRATION_CONNECTED_MAIL_DEFINITION_FILE"
	ConnectedMailCallbackURIEnv       = "NEXUSCRM_INTEGRATION_CONNECTED_MAIL_CALLBACK_URI"
	ConnectedMailReturnBaseURLEnv     = "NEXUSCRM_INTEGRATION_CONNECTED_MAIL_RETURN_BASE_URL"
	ConnectedMailOAuthClientIDEnv     = "NEXUSCRM_INTEGRATION_CONNECTED_MAIL_OAUTH_CLIENT_ID"
	ConnectedMailOAuthClientSecretEnv = "NEXUSCRM_INTEGRATION_CONNECTED_MAIL_OAUTH_CLIENT_SECRET"
	ConnectedMailMasterKeyEnv         = "NEXUSCRM_INTEGRATION_CONNECTED_MAIL_MASTER_KEY"

	connectedMailDefinitionSchemaV1 = "nexuscrm.connected-mail-http-provider/v1"
	maxConnectedMailDefinitionBytes = 1024 * 1024
)

type ConnectedMailConfig struct {
	Definition        ConnectedMailDefinition
	CallbackURI       string
	ReturnBaseURL     string
	OAuthClientID     string
	OAuthClientSecret string
	MasterKey         []byte
}

type ConnectedMailDefinitionBinding struct {
	OwnerPack           string `json:"ownerPack"`
	BusinessKey         string `json:"businessKey"`
	DefinitionVersion   uint64 `json:"definitionVersion"`
	ClientCredentialRef string `json:"clientCredentialRef"`
}

// ConnectedMailDefinition is secret-free provider configuration mounted by a
// release. Credential values and exact installed-pack generation identity
// remain process inputs.
type ConnectedMailDefinition struct {
	Schema                   string                         `json:"schema"`
	ConnectorKey             string                         `json:"connectorKey"`
	PublicPath               string                         `json:"publicPath"`
	Binding                  ConnectedMailDefinitionBinding `json:"binding"`
	RequiredScopes           []string                       `json:"requiredScopes"`
	AuthorizationParameters  map[string]string              `json:"authorizationParameters,omitempty"`
	AuthorizationEndpoint    string                         `json:"authorizationEndpoint"`
	TokenEndpoint            string                         `json:"tokenEndpoint"`
	IdentityEndpoint         string                         `json:"identityEndpoint"`
	RevocationEndpoint       string                         `json:"revocationEndpoint"`
	MailEndpoint             string                         `json:"mailEndpoint"`
	Issuer                   string                         `json:"issuer"`
	IdentitySubjectField     string                         `json:"identitySubjectField"`
	IdentityAddressField     string                         `json:"identityAddressField"`
	IdentityDisplayNameField string                         `json:"identityDisplayNameField"`
	MailRequestField         string                         `json:"mailRequestField"`
	MailResponseIDField      string                         `json:"mailResponseIdField"`
	TerminationBound         string                         `json:"terminationBound"`

	terminationDuration time.Duration
}

func (definition ConnectedMailDefinition) OAuthConfig(clientID, clientSecret string) connectedmailhttp.OAuthConfig {
	return connectedmailhttp.OAuthConfig{
		AuthorizationEndpoint: definition.AuthorizationEndpoint,
		TokenEndpoint:         definition.TokenEndpoint, IdentityEndpoint: definition.IdentityEndpoint,
		RevocationEndpoint: definition.RevocationEndpoint, Issuer: definition.Issuer,
		ClientID: clientID, ClientSecret: clientSecret,
		AuthorizationParameters:  cloneConnectedMailParameters(definition.AuthorizationParameters),
		IdentitySubjectField:     definition.IdentitySubjectField,
		IdentityAddressField:     definition.IdentityAddressField,
		IdentityDisplayNameField: definition.IdentityDisplayNameField,
		TerminationBound:         definition.terminationDuration,
	}
}

func (definition ConnectedMailDefinition) MailConfig() connectedmailhttp.MailConfig {
	return connectedmailhttp.MailConfig{
		Endpoint: definition.MailEndpoint, RequestField: definition.MailRequestField,
		ResponseIDField: definition.MailResponseIDField, TerminationBound: definition.terminationDuration,
	}
}

func (definition ConnectedMailDefinition) MailBindingDefinition(
	view catalogv1.IntegrationBindingView,
) (connectoroutboundintent.MailBindingDefinition, error) {
	if catalogv1.ValidateIntegrationBindingView(view) != nil {
		return connectoroutboundintent.MailBindingDefinition{}, fmt.Errorf("connected-mail Catalog view is invalid")
	}
	var selected *catalogv1.IntegrationBinding
	for index := range view.Bindings {
		binding := view.Bindings[index]
		if binding.Kind != catalogv1.BindingConnectorCalloutProfile ||
			binding.OwnerPack != definition.Binding.OwnerPack ||
			binding.BusinessKey != definition.Binding.BusinessKey ||
			binding.DefinitionVersion != definition.Binding.DefinitionVersion {
			continue
		}
		if selected != nil {
			return connectoroutboundintent.MailBindingDefinition{}, fmt.Errorf("connected-mail Catalog binding is ambiguous")
		}
		selected = &view.Bindings[index]
	}
	if selected == nil || !containsConnectedMailString(selected.CredentialRefs, definition.Binding.ClientCredentialRef) {
		return connectoroutboundintent.MailBindingDefinition{}, fmt.Errorf("connected-mail Catalog binding is unavailable")
	}
	return connectoroutboundintent.MailBindingDefinition{
		OwnerPack: definition.Binding.OwnerPack, BusinessKey: definition.Binding.BusinessKey,
		DefinitionVersion:   definition.Binding.DefinitionVersion,
		ConnectorKey:        connectedMailRuntimeConnectorKey(definition, *selected),
		ClientCredentialRef: definition.Binding.ClientCredentialRef,
		RequiredScopes:      append([]string(nil), definition.RequiredScopes...),
	}, nil
}

func loadConnectedMailConfig(lookup LookupEnv, required func(string) (string, error)) (*ConnectedMailConfig, error) {
	names := []string{
		ConnectedMailDefinitionFileEnv, ConnectedMailCallbackURIEnv, ConnectedMailReturnBaseURLEnv,
		ConnectedMailOAuthClientIDEnv, ConnectedMailOAuthClientSecretEnv, ConnectedMailMasterKeyEnv,
	}
	present := false
	for _, name := range names {
		if _, ok := lookup(name); ok {
			present = true
			break
		}
	}
	if !present {
		return nil, nil
	}
	values := make(map[string]string, len(names))
	for _, name := range names {
		value, err := required(name)
		if err != nil {
			return nil, err
		}
		values[name] = value
	}
	definition, err := readConnectedMailDefinition(values[ConnectedMailDefinitionFileEnv])
	if err != nil {
		return nil, fmt.Errorf("integration service: %s is invalid: %w", ConnectedMailDefinitionFileEnv, err)
	}
	callbackURI := values[ConnectedMailCallbackURIEnv]
	returnBaseURL := values[ConnectedMailReturnBaseURLEnv]
	if !validConnectedMailPublicURL(callbackURI, false) {
		return nil, fmt.Errorf("integration service: %s is invalid", ConnectedMailCallbackURIEnv)
	}
	callback, _ := url.Parse(callbackURI)
	if callback.Path != definition.PublicPath+"/callback" {
		return nil, fmt.Errorf("integration service: %s does not match the provider definition", ConnectedMailCallbackURIEnv)
	}
	if !validConnectedMailPublicURL(returnBaseURL, true) {
		return nil, fmt.Errorf("integration service: %s is invalid", ConnectedMailReturnBaseURLEnv)
	}
	if !connectedMailSecretValue(values[ConnectedMailOAuthClientIDEnv], 4096) ||
		!connectedMailSecretValue(values[ConnectedMailOAuthClientSecretEnv], 16*1024) {
		return nil, fmt.Errorf("integration service: connected-mail OAuth client configuration is invalid")
	}
	masterKey, err := base64.StdEncoding.DecodeString(values[ConnectedMailMasterKeyEnv])
	if err != nil || len(masterKey) != 32 {
		clearConnectedMailBytes(masterKey)
		return nil, fmt.Errorf("integration service: %s must contain an AES-256 key", ConnectedMailMasterKeyEnv)
	}
	return &ConnectedMailConfig{
		Definition: definition, CallbackURI: callbackURI, ReturnBaseURL: returnBaseURL,
		OAuthClientID: values[ConnectedMailOAuthClientIDEnv], OAuthClientSecret: values[ConnectedMailOAuthClientSecretEnv],
		MasterKey: masterKey,
	}, nil
}

func readConnectedMailDefinition(path string) (ConnectedMailDefinition, error) {
	if !filepath.IsAbs(path) || filepath.Clean(path) != path {
		return ConnectedMailDefinition{}, fmt.Errorf("definition path must be absolute and canonical")
	}
	info, err := os.Stat(path)
	if err != nil || !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > maxConnectedMailDefinitionBytes {
		return ConnectedMailDefinition{}, fmt.Errorf("definition must be one bounded regular file")
	}
	raw, err := os.ReadFile(path)
	if err != nil {
		return ConnectedMailDefinition{}, fmt.Errorf("read definition: %w", err)
	}
	defer clearConnectedMailBytes(raw)
	return parseConnectedMailDefinition(raw)
}

func parseConnectedMailDefinition(raw []byte) (ConnectedMailDefinition, error) {
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var definition ConnectedMailDefinition
	if len(raw) == 0 || decoder.Decode(&definition) != nil || decoder.Decode(&struct{}{}) != io.EOF {
		return ConnectedMailDefinition{}, fmt.Errorf("definition JSON is invalid")
	}
	scopes, ok := canonicalConnectedMailScopes(definition.RequiredScopes)
	duration, durationErr := time.ParseDuration(definition.TerminationBound)
	if !ok || durationErr != nil || duration <= 0 || duration > time.Minute ||
		definition.Schema != connectedMailDefinitionSchemaV1 || !connectedMailValue(definition.ConnectorKey, 255) ||
		!validConnectedMailPublicPath(definition.PublicPath) || !connectedMailValue(definition.Binding.OwnerPack, 255) ||
		!connectedMailValue(definition.Binding.BusinessKey, 255) || definition.Binding.DefinitionVersion == 0 ||
		!connectedMailValue(definition.Binding.ClientCredentialRef, 255) ||
		!validConnectedMailParameters(definition.AuthorizationParameters) ||
		!validConnectedMailEndpoint(definition.AuthorizationEndpoint) || !validConnectedMailEndpoint(definition.TokenEndpoint) ||
		!validConnectedMailEndpoint(definition.IdentityEndpoint) || !validConnectedMailEndpoint(definition.RevocationEndpoint) ||
		!validConnectedMailEndpoint(definition.MailEndpoint) || !validConnectedMailEndpoint(definition.Issuer) ||
		!validConnectedMailField(definition.IdentitySubjectField) || !validConnectedMailField(definition.IdentityAddressField) ||
		!validConnectedMailField(definition.IdentityDisplayNameField) || !validConnectedMailField(definition.MailRequestField) ||
		!validConnectedMailField(definition.MailResponseIDField) {
		return ConnectedMailDefinition{}, fmt.Errorf("definition values are invalid")
	}
	definition.RequiredScopes = scopes
	definition.terminationDuration = duration
	return definition, nil
}

func connectedMailRuntimeConnectorKey(
	definition ConnectedMailDefinition,
	binding catalogv1.IntegrationBinding,
) string {
	material := strings.Join([]string{
		definition.ConnectorKey, string(binding.Kind), binding.OwnerPack, binding.PackInstallationID,
		binding.PackPayloadDigest, binding.BusinessKey, strconv.FormatUint(binding.DefinitionVersion, 10),
		definition.Binding.ClientCredentialRef,
	}, "\x00")
	digest := sha256.Sum256([]byte(material))
	return "connected-mail/sha256:" + hex.EncodeToString(digest[:])
}

func containsConnectedMailString(values []string, expected string) bool {
	for _, value := range values {
		if value == expected {
			return true
		}
	}
	return false
}

func validConnectedMailEndpoint(value string) bool {
	parsed, err := url.Parse(value)
	return err == nil && parsed.Scheme == "https" && parsed.User == nil && parsed.Host != "" &&
		parsed.RawQuery == "" && parsed.Fragment == "" && parsed.Opaque == ""
}

func validConnectedMailPublicURL(value string, baseOnly bool) bool {
	parsed, err := url.Parse(value)
	if err != nil || parsed.User != nil || parsed.Host == "" || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Opaque != "" {
		return false
	}
	host := parsed.Hostname()
	if parsed.Scheme != "https" && !(parsed.Scheme == "http" && (host == "127.0.0.1" || host == "::1" || host == "localhost")) {
		return false
	}
	return !baseOnly || parsed.Path == "" || parsed.Path == "/"
}

func validConnectedMailPublicPath(value string) bool {
	return strings.HasPrefix(value, "/api/connected-mailboxes/") && len(value) <= 255 &&
		value == path.Clean(value) && value == strings.TrimRight(value, "/") &&
		!strings.ContainsAny(value, "?#\\\x00\r\n")
}

func validConnectedMailField(value string) bool {
	if !connectedMailValue(value, 128) {
		return false
	}
	for _, current := range value {
		if !((current >= 'a' && current <= 'z') || (current >= 'A' && current <= 'Z') ||
			(current >= '0' && current <= '9') || current == '_' || current == '-') {
			return false
		}
	}
	return true
}

func validConnectedMailParameters(values map[string]string) bool {
	if len(values) > 2 {
		return false
	}
	for key, value := range values {
		switch key {
		case "access_type":
			if value != "offline" && value != "online" {
				return false
			}
		case "prompt":
			parts := strings.Fields(value)
			if len(parts) == 0 || strings.Join(parts, " ") != value {
				return false
			}
			seen := make(map[string]struct{}, len(parts))
			for _, part := range parts {
				switch part {
				case "none", "login", "consent", "select_account":
				default:
					return false
				}
				if _, duplicate := seen[part]; duplicate || part == "none" && len(parts) != 1 {
					return false
				}
				seen[part] = struct{}{}
			}
		default:
			return false
		}
	}
	return true
}

func canonicalConnectedMailScopes(values []string) ([]string, bool) {
	if len(values) == 0 || len(values) > 64 {
		return nil, false
	}
	seen := make(map[string]struct{}, len(values))
	result := make([]string, 0, len(values))
	for _, value := range values {
		if !connectedMailValue(value, 4096) || strings.IndexFunc(value, unicode.IsSpace) >= 0 {
			return nil, false
		}
		if _, duplicate := seen[value]; duplicate {
			return nil, false
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	sort.Strings(result)
	return result, true
}

func connectedMailValue(value string, maximum int) bool {
	return value != "" && value == strings.TrimSpace(value) && len(value) <= maximum && utf8.ValidString(value) &&
		strings.IndexFunc(value, unicode.IsControl) < 0
}

func connectedMailSecretValue(value string, maximum int) bool {
	return value != "" && value == strings.TrimSpace(value) && len(value) <= maximum &&
		!strings.ContainsAny(value, "\x00\r\n")
}

func cloneConnectedMailParameters(values map[string]string) map[string]string {
	if len(values) == 0 {
		return nil
	}
	cloned := make(map[string]string, len(values))
	for key, value := range values {
		cloned[key] = value
	}
	return cloned
}

func clearConnectedMailBytes(value []byte) {
	for index := range value {
		value[index] = 0
	}
}
