package app

import (
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/extension/providers/searchengine/runtime"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

const searchEngineConnectionExternalSubjectRef = "default"

const (
	maxSearchEngineAPIKeyBytes = 16 * 1024
	maxSearXNGBaseURLBytes     = 2 * 1024
)

type searchEngineProduct struct {
	AppID       string
	AppSlug     string
	Provider    string
	DisplayName string
	RequiresKey bool
}

var searchEngineProductsByProvider = map[string]searchEngineProduct{
	runtime.ProviderSearXNG: {
		AppID:       registry.SearXNGAppID,
		AppSlug:     registry.SearXNGAppSlug,
		Provider:    runtime.ProviderSearXNG,
		DisplayName: "SearXNG",
	},
	runtime.ProviderBrave: {
		AppID:       registry.BraveSearchAppID,
		AppSlug:     registry.BraveSearchAppSlug,
		Provider:    runtime.ProviderBrave,
		DisplayName: "Brave Search",
		RequiresKey: true,
	},
	runtime.ProviderTavily: {
		AppID:       registry.TavilyAppID,
		AppSlug:     registry.TavilyAppSlug,
		Provider:    runtime.ProviderTavily,
		DisplayName: "Tavily Search",
		RequiresKey: true,
	},
	runtime.ProviderSerper: {
		AppID:       registry.SerperAppID,
		AppSlug:     registry.SerperAppSlug,
		Provider:    runtime.ProviderSerper,
		DisplayName: "Serper Search",
		RequiresKey: true,
	},
}

// SearchEngineConnectionConfigView is the non-secret provider configuration.
type SearchEngineConnectionConfigView struct {
	BaseURL  string
	Language string
}

// SearchEngineConnectionView is the secret-free App management projection.
type SearchEngineConnectionView struct {
	Provider         string
	Active           bool
	Config           SearchEngineConnectionConfigView
	APIKeyConfigured bool
	UpdatedAt        *time.Time
}

// UpsertSearchEngineConnectionParams contains closed provider-owned config and
// credentials objects. Credentials is write-only at the API boundary.
type UpsertSearchEngineConnectionParams struct {
	Config      map[string]any
	Credentials map[string]any
}

type preparedSearchEngineConnection struct {
	Config         SearchEngineConnectionConfigView
	APIKey         string
	APIKeyProvided bool
}

func searchEngineProductForDefinition(
	appID string,
	appSlug string,
) (searchEngineProduct, bool) {
	appID = strings.TrimSpace(appID)
	appSlug = strings.ToLower(strings.TrimSpace(appSlug))
	for _, product := range searchEngineProductsByProvider {
		if appID == product.AppID && appSlug == product.AppSlug {
			return product, true
		}
	}
	return searchEngineProduct{}, false
}

func searchEngineInstallationMatches(
	installation serviceactor.Installation,
	workspaceID string,
	product searchEngineProduct,
) bool {
	return installation.WorkspaceID == workspaceID &&
		installation.ServiceDefinitionID == product.AppID &&
		installation.ServiceKind == serviceactor.KindApp &&
		installation.Status == serviceactor.StatusActive
}

func prepareSearchEngineConnection(
	product searchEngineProduct,
	params UpsertSearchEngineConnectionParams,
) (preparedSearchEngineConnection, error) {
	config, err := parseSearchEngineConnectionConfig(product, params.Config)
	if err != nil {
		return preparedSearchEngineConnection{}, err
	}
	allowedCredentialFields := []string(nil)
	if product.RequiresKey {
		allowedCredentialFields = []string{"apiKey"}
	}
	if err := rejectUnknownSearchEngineFields(
		"credentials",
		params.Credentials,
		allowedCredentialFields,
	); err != nil {
		return preparedSearchEngineConnection{}, err
	}
	prepared := preparedSearchEngineConnection{Config: config}
	rawAPIKey, provided := params.Credentials["apiKey"]
	if !provided {
		return prepared, nil
	}
	apiKey, ok := rawAPIKey.(string)
	if !ok {
		return preparedSearchEngineConnection{}, badRequest("credentials.apiKey must be a string")
	}
	apiKey = strings.TrimSpace(apiKey)
	if apiKey == "" || strings.ContainsAny(apiKey, "\r\n") {
		return preparedSearchEngineConnection{}, badRequest("credentials.apiKey must be non-empty")
	}
	if len([]byte(apiKey)) > maxSearchEngineAPIKeyBytes {
		return preparedSearchEngineConnection{}, badRequest("credentials.apiKey is too long")
	}
	prepared.APIKey = apiKey
	prepared.APIKeyProvided = true
	return prepared, nil
}

func parseSearchEngineConnectionConfig(
	product searchEngineProduct,
	config map[string]any,
) (SearchEngineConnectionConfigView, error) {
	if product.Provider != runtime.ProviderSearXNG {
		if err := rejectUnknownSearchEngineFields("config", config, nil); err != nil {
			return SearchEngineConnectionConfigView{}, err
		}
		return SearchEngineConnectionConfigView{}, nil
	}
	if err := rejectUnknownSearchEngineFields(
		"config",
		config,
		[]string{"baseUrl", "language"},
	); err != nil {
		return SearchEngineConnectionConfigView{}, err
	}
	baseURL, ok := config["baseUrl"].(string)
	baseURL = strings.TrimSpace(baseURL)
	if !ok || baseURL == "" {
		return SearchEngineConnectionConfigView{}, badRequest("config.baseUrl is required")
	}
	if len([]byte(baseURL)) > maxSearXNGBaseURLBytes {
		return SearchEngineConnectionConfigView{}, badRequest("config.baseUrl is too long")
	}
	if err := runtime.ValidateSearXNGBaseURL(baseURL); err != nil {
		return SearchEngineConnectionConfigView{}, badRequest("config.baseUrl is invalid")
	}
	language := ""
	if rawLanguage, found := config["language"]; found {
		var languageOK bool
		language, languageOK = rawLanguage.(string)
		if !languageOK {
			return SearchEngineConnectionConfigView{}, badRequest("config.language must be a string")
		}
		language = strings.TrimSpace(language)
		if len([]rune(language)) > 64 || strings.ContainsAny(language, "\r\n") {
			return SearchEngineConnectionConfigView{}, badRequest("config.language is invalid")
		}
	}
	return SearchEngineConnectionConfigView{
		BaseURL:  baseURL,
		Language: language,
	}, nil
}

func rejectUnknownSearchEngineFields(
	label string,
	object map[string]any,
	allowed []string,
) error {
	allowedSet := make(map[string]struct{}, len(allowed))
	for _, field := range allowed {
		allowedSet[field] = struct{}{}
	}
	for field := range object {
		if _, ok := allowedSet[field]; !ok {
			return badRequest(fmt.Sprintf("%s.%s is not supported", label, field))
		}
	}
	return nil
}

func encodeSearchEngineConnectionConfig(
	product searchEngineProduct,
	config SearchEngineConnectionConfigView,
) ([]byte, error) {
	value := map[string]any{}
	if product.Provider == runtime.ProviderSearXNG {
		value["baseUrl"] = config.BaseURL
		if config.Language != "" {
			value["language"] = config.Language
		}
	}
	return model.MarshalJSONObject("search engine connection config", value)
}

func decodeSearchEngineConnectionConfig(
	product searchEngineProduct,
	payload []byte,
) (SearchEngineConnectionConfigView, error) {
	var value map[string]any
	if err := model.UnmarshalJSONObject(
		"search engine connection config",
		payload,
		&value,
	); err != nil {
		return SearchEngineConnectionConfigView{}, fmt.Errorf("decode search engine connection config: %w", err)
	}
	return parseSearchEngineConnectionConfig(product, value)
}

func requireAuthoritativeSearchEngineConnection(
	installation serviceactor.Installation,
	product searchEngineProduct,
	connections []serviceactor.Connection,
) (*serviceactor.Connection, error) {
	if len(connections) == 0 {
		return nil, nil
	}
	if len(connections) != 1 {
		return nil, conflict("Search engine App has an ambiguous active connection.")
	}
	connection := connections[0]
	if connection.WorkspaceID != installation.WorkspaceID ||
		connection.ServiceDefinitionID != product.AppID ||
		connection.ServiceInstallationID != installation.ID ||
		connection.ServiceKind != serviceactor.KindApp ||
		connection.ExternalSubjectRef != searchEngineConnectionExternalSubjectRef ||
		connection.Status != serviceactor.StatusActive {
		return nil, conflict("Search engine App connection identity is invalid.")
	}
	return &connection, nil
}

func (store *Store) searchEngineAPIKey(
	connection serviceactor.Connection,
) (string, error) {
	plaintext, err := store.decryptStoredAppCredentialsJSON(connection.Credentials)
	if err != nil {
		return "", err
	}
	var credentials map[string]any
	if err := model.UnmarshalJSONObject(
		"search engine connection credentials",
		plaintext,
		&credentials,
	); err != nil {
		return "", fmt.Errorf("decode search engine connection credentials: %w", err)
	}
	if err := rejectUnknownSearchEngineFields(
		"stored credentials",
		credentials,
		[]string{"apiKey"},
	); err != nil {
		return "", err
	}
	rawAPIKey, found := credentials["apiKey"]
	if !found {
		return "", nil
	}
	apiKey, ok := rawAPIKey.(string)
	if !ok {
		return "", fmt.Errorf("stored search engine api key is invalid")
	}
	return strings.TrimSpace(apiKey), nil
}

func (store *Store) searchEngineConnectionView(
	installation serviceactor.Installation,
	product searchEngineProduct,
	connections []serviceactor.Connection,
) (SearchEngineConnectionView, error) {
	connection, err := requireAuthoritativeSearchEngineConnection(
		installation,
		product,
		connections,
	)
	if err != nil {
		return SearchEngineConnectionView{}, err
	}
	if connection == nil {
		return SearchEngineConnectionView{
			Provider: product.Provider,
			Active:   false,
			Config:   SearchEngineConnectionConfigView{},
		}, nil
	}
	config, err := decodeSearchEngineConnectionConfig(product, connection.Config)
	if err != nil {
		return SearchEngineConnectionView{}, err
	}
	apiKey, err := store.searchEngineAPIKey(*connection)
	if err != nil {
		return SearchEngineConnectionView{}, err
	}
	updatedAt := connection.UpdatedAt.UTC()
	return SearchEngineConnectionView{
		Provider:         product.Provider,
		Active:           true,
		Config:           config,
		APIKeyConfigured: apiKey != "",
		UpdatedAt:        &updatedAt,
	}, nil
}
