package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"syscall"
	"time"

	apihostmiddleware "github.com/pingcap-inc/nexuscrm-api-host/middleware"
	"github.com/pingcap-inc/nexuscrm-api-host/processhttp"
	"github.com/pingcap-inc/nexuscrm-api-host/runtimegrpc"
	appservice "github.com/pingcap-inc/nexuscrm-app-packs/appservices/sfcrm"
	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	bulkingestsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/bulkingestv1"
	filessemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/filesv1"
)

var (
	version = "dev"
	commit  = "unknown"
)

const runtimeAdmissionRetryBudget = 750 * time.Millisecond

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() error {
	config, err := loadConfig(os.LookupEnv)
	if err != nil {
		return err
	}
	if !config.requireFederatedLoginState {
		log.Print("WARNING: SFCRM_REQUIRE_FEDERATED_LOGIN_STATE=false; pre-upgrade Google callbacks are temporarily admitted without server-owned login state")
	}
	// Every neutral Runtime client is one transport facet of the same exact
	// Runtime/workload authority. The group shares bounded Identity delegations
	// across those facets without allowing a different target or workload
	// credential to reuse them. Runtime still admits every data RPC against the
	// delegation and current security/session floors.
	runtimeClients, err := runtimegrpc.NewRuntimeClientGroupWithOptions(
		config.runtime,
		runtimegrpc.WithRecordMutationAdmissionRetryBudget(runtimeAdmissionRetryBudget),
	)
	if err != nil {
		return err
	}
	startupCtx, cancelStartup := context.WithTimeout(context.Background(), config.startupTimeout)
	recordAccessRuntime, err := runtimeClients.DialRecordAccessRuntime(startupCtx)
	if err != nil {
		cancelStartup()
		return err
	}
	queryRuntime, err := runtimeClients.DialQueryRuntime(startupCtx)
	if err != nil {
		cancelStartup()
		_ = recordAccessRuntime.Close()
		return err
	}
	mutationRuntime, err := runtimeClients.DialRecordMutationRuntime(startupCtx)
	if err != nil {
		cancelStartup()
		_ = queryRuntime.Close()
		_ = recordAccessRuntime.Close()
		return err
	}
	createDefaultRuntime, err := runtimeClients.DialCreateDefaultPreviewRuntime(startupCtx)
	if err != nil {
		cancelStartup()
		_ = mutationRuntime.Close()
		_ = queryRuntime.Close()
		_ = recordAccessRuntime.Close()
		return err
	}
	relatedCountRuntime, err := runtimeClients.DialRelatedCountRuntime(startupCtx)
	if err != nil {
		cancelStartup()
		_ = createDefaultRuntime.Close()
		_ = mutationRuntime.Close()
		_ = queryRuntime.Close()
		_ = recordAccessRuntime.Close()
		return err
	}
	recycleBinRuntime, err := runtimeClients.DialRecycleBinRuntime(startupCtx)
	if err != nil {
		cancelStartup()
		_ = relatedCountRuntime.Close()
		_ = createDefaultRuntime.Close()
		_ = mutationRuntime.Close()
		_ = queryRuntime.Close()
		_ = recordAccessRuntime.Close()
		return err
	}
	recordViewRuntime, err := runtimeClients.DialRecordViewProofRuntime(startupCtx)
	if err != nil {
		cancelStartup()
		_ = recycleBinRuntime.Close()
		_ = relatedCountRuntime.Close()
		_ = createDefaultRuntime.Close()
		_ = mutationRuntime.Close()
		_ = queryRuntime.Close()
		_ = recordAccessRuntime.Close()
		return err
	}
	platformRuntime, err := runtimeClients.DialPlatformRuntime(startupCtx)
	cancelStartup()
	if err != nil {
		_ = recordViewRuntime.Close()
		_ = recycleBinRuntime.Close()
		_ = relatedCountRuntime.Close()
		_ = createDefaultRuntime.Close()
		_ = mutationRuntime.Close()
		_ = queryRuntime.Close()
		_ = recordAccessRuntime.Close()
		return err
	}
	defer recordAccessRuntime.Close()
	defer queryRuntime.Close()
	defer mutationRuntime.Close()
	defer createDefaultRuntime.Close()
	defer relatedCountRuntime.Close()
	defer recycleBinRuntime.Close()
	defer recordViewRuntime.Close()
	defer platformRuntime.Close()
	appMetadataStartupCtx, cancelAppMetadataStartup := context.WithTimeout(
		context.Background(), config.startupTimeout,
	)
	appMetadata, err := appservice.NewRuntimeAppMetadataV3Cache(
		appMetadataStartupCtx,
		platformRuntime.GetAppMetadataViewV3,
		func(
			ctx context.Context,
			request catalogsemantic.WatchRequest,
		) (appservice.RuntimeAppMetadataV3Watch, error) {
			return platformRuntime.WatchAppMetadataViewV3(ctx, request)
		},
	)
	cancelAppMetadataStartup()
	if err != nil {
		return err
	}
	defer appMetadata.Close()
	googleExchange, err := appservice.NewGoogleCodeExchange(
		&http.Client{Timeout: config.googleTimeout}, config.googleClientID, config.googleClientSecret,
	)
	if err != nil && (config.googleClientID != "" || config.googleClientSecret != "") {
		return err
	}
	service, err := appservice.NewService(appservice.ServiceConfig{
		Session: &appservice.SessionHTTPConfig{
			RuntimeCreateSession:         platformRuntime.CreateSession,
			RuntimeReadSession:           platformRuntime.ReadSession,
			RuntimeRevokeSession:         platformRuntime.RevokeSession,
			RuntimeBeginAuthorization:    platformRuntime.BeginAuthorization,
			RuntimeCompleteAuthorization: platformRuntime.CompleteAuthorization,
			RuntimeExchangeAuthorization: platformRuntime.ExchangeAuthorizationToken,
			RuntimeBeginFederatedLogin:   platformRuntime.BeginFederatedLogin,
			RuntimeConsumeFederatedLogin: platformRuntime.ConsumeFederatedLogin,
			RuntimeReadEffective:         platformRuntime.ReadEffectivePermissions,
			FederatedCodeExchange:        googleExchange,
			PublicOrigin:                 config.publicOrigin, OrganizationID: config.organizationID,
			GoogleClientID: config.googleClientID, LoginMethods: config.loginMethods,
			PasswordGrantEnabled:         config.passwordGrantEnabled,
			PasswordBreakGlassUsers:      config.passwordBreakGlassUsers,
			RequireBrowserSessionControl: true,
			RequireFederatedLoginState:   config.requireFederatedLoginState,
		},
		RuntimeEvaluate: recordAccessRuntime.Evaluate,
		RuntimeQuery:    queryRuntime.Query, RuntimeHistoryQuery: queryRuntime.QueryHistory,
		RuntimeMutate:                        mutationRuntime.Mutate,
		RuntimeRelatedCount:                  relatedCountRuntime.Count,
		RuntimeRecycleBin:                    recycleBinRuntime.List,
		RuntimeRestoreRecycleBin:             recycleBinRuntime.Restore,
		RuntimePurgeRecycleBin:               recycleBinRuntime.Purge,
		RuntimeReadRecordView:                recordViewRuntime.Read,
		RuntimeResolveAttachmentSourceProofs: recordViewRuntime.ResolveAttachmentSourceProofs,
		RuntimeBeginFileCreate: func(ctx context.Context, bearer string, header filessemantic.CreateHeader) (appservice.RuntimeFileUpload, error) {
			return platformRuntime.BeginCreateFile(ctx, bearer, header)
		},
		RuntimeOpenFile: func(ctx context.Context, bearer string, request filessemantic.OpenRequest) (appservice.RuntimeFileDownload, error) {
			return platformRuntime.OpenFile(ctx, bearer, request)
		},
		RuntimeDeleteFile:            platformRuntime.DeleteFile,
		RuntimeCreateBulkIngestJob:   platformRuntime.CreateBulkIngestJob,
		RuntimeReadBulkIngestJob:     platformRuntime.ReadBulkIngestJob,
		RuntimeCloseBulkIngestJob:    platformRuntime.CloseBulkIngestJob,
		RuntimePreviewCreateDefaults: createDefaultRuntime.Preview,
		RuntimeBeginBulkUpload: func(ctx context.Context, bearer string, header bulkingestsemantic.UploadHeader) (appservice.RuntimeBulkIngestUpload, error) {
			return platformRuntime.BeginBulkIngestUpload(ctx, bearer, header)
		},
		RuntimeReadEffectivePermissions:        platformRuntime.ReadEffectivePermissions,
		RuntimeAuthorizeCapabilities:           platformRuntime.AuthorizeCapabilities,
		RuntimeGetAppMetadataViewV3:            appMetadata.Get,
		RuntimeResolvePageLayout:               platformRuntime.ResolvePageLayout,
		RuntimeReadObjectListFields:            platformRuntime.ReadObjectListFields,
		RuntimeReconcileAppMetadataViewV3:      appMetadata.ReconcileReceipts,
		RuntimeReadSearchScopeOrder:            platformRuntime.ReadSearchScopeOrder,
		RuntimeSearch:                          platformRuntime.Search,
		RuntimeExecuteFlow:                     platformRuntime.ExecuteFlowV2WithAppMetadataV3,
		RuntimeExecuteCatalogAction:            platformRuntime.ExecuteCatalogActionV2WithAppMetadataV3,
		RuntimePreviewCatalogAction:            platformRuntime.PreviewCatalogActionWithAppMetadataV3,
		EnableUIApps:                           true,
		RuntimeReadApplicationNavigation:       platformRuntime.ReadApplicationNavigation,
		RuntimeResolveApplicationNavigation:    platformRuntime.ResolveApplicationNavigation,
		RuntimeSaveApplicationNavigation:       platformRuntime.SaveApplicationNavigation,
		RuntimeReadReport:                      platformRuntime.ReadReport,
		RuntimeReadAuditLog:                    platformRuntime.ReadAuditLog,
		RuntimeExecuteApproval:                 platformRuntime.ExecuteApproval,
		RuntimeReadApproval:                    platformRuntime.ReadApproval,
		RuntimeReadNotificationFlows:           platformRuntime.ReadApprovalNotificationFlows,
		RuntimeReadPrivateListViews:            platformRuntime.ReadPrivateListViews,
		RuntimeMutatePrivateListView:           platformRuntime.MutatePrivateListView,
		RuntimeReadPermissionAuthoringSnapshot: platformRuntime.ReadPermissionAuthoringSnapshot,
		RuntimeSavePermissionAuthoring:         platformRuntime.SavePermissionAuthoring,
		RuntimeGetPermissionAuthoringStatus:    platformRuntime.GetPermissionAuthoringStatus,
		RuntimeReadObjectAdminIndex:            platformRuntime.ReadObjectAdminIndex,
		RuntimeReadValidationRuleAdminIndex:    platformRuntime.ReadValidationRuleAdminIndex,
		RuntimeReadLayoutAuthoringWorkspace:    platformRuntime.ReadLayoutAuthoringV2Workspace,
		RuntimeReadLayoutAuthoringStored:       platformRuntime.ReadLayoutAuthoringV2StoredLayout,
		RuntimeMutateLayoutAuthoring:           platformRuntime.MutateLayoutAuthoringV2,
		RuntimeReady: func() bool {
			return recordAccessRuntime.Ready() && queryRuntime.Ready() && mutationRuntime.Ready() && createDefaultRuntime.Ready() && relatedCountRuntime.Ready() && recycleBinRuntime.Ready() && recordViewRuntime.Ready() && platformRuntime.Ready()
		},
		RuntimeMetrics: func() runtimegrpc.RuntimeClientMetrics {
			access, query, mutation, defaults, counts, recycle, view := recordAccessRuntime.Metrics(), queryRuntime.Metrics(), mutationRuntime.Metrics(), createDefaultRuntime.Metrics(), relatedCountRuntime.Metrics(), recycleBinRuntime.Metrics(), recordViewRuntime.Metrics()
			return runtimegrpc.RuntimeClientMetrics{
				IdentityCalls: access.IdentityCalls + query.IdentityCalls + mutation.IdentityCalls + defaults.IdentityCalls + counts.IdentityCalls + recycle.IdentityCalls + view.IdentityCalls,
				RecordsCalls:  access.RecordsCalls + query.RecordsCalls + mutation.RecordsCalls + defaults.RecordsCalls + counts.RecordsCalls + recycle.RecordsCalls + view.RecordsCalls,
			}
		},
		AppMetadataMetrics: appMetadata.Metrics,
		RequestTimeout:     config.requestTimeout, MaxRequestBytes: config.maxRequestBytes,
	})
	if err != nil {
		return err
	}
	capabilities := processCapabilities()
	sessionAdmission, err := apihostmiddleware.NewSessionControlHTTPAdmission(config.sessionControlOrigins...)
	if err != nil {
		return err
	}
	handler := apihostmiddleware.ProtectSessionCredentialResponses(sessionAdmission(service))
	process, err := processhttp.NewProcess(processhttp.ProcessConfig{
		Role: "sfcrm-appservice", Version: version + "+" + commit, Address: config.httpAddress,
		Capabilities: capabilities, Handler: handler,
		Ready: service.Ready, BeginDrain: service.BeginDrain,
		Policy: processhttp.ServerPolicy{
			ReadHeaderTimeout: config.readHeaderTimeout, ReadTimeout: config.requestTimeout,
			WriteTimeout: config.requestTimeout, IdleTimeout: config.idleTimeout,
		},
	})
	if err != nil {
		return err
	}
	if err := process.Start(); err != nil {
		return err
	}
	log.Printf("sfcrm AppService started: address=%s app=%s capabilities=%s version=%s commit=%s", process.Address(), appservice.AppID, strings.Join(capabilities, ","), version, commit)

	signalCtx, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stopSignals()
	serveResult := make(chan error, 1)
	go func() { serveResult <- process.Wait() }()
	select {
	case <-signalCtx.Done():
	case err := <-serveResult:
		return err
	}
	shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), config.shutdownTimeout)
	defer cancelShutdown()
	if err := process.Shutdown(shutdownCtx); err != nil {
		return fmt.Errorf("sfcrm AppService: shutdown: %w", err)
	}
	if err := <-serveResult; err != nil {
		return err
	}
	return nil
}

func processCapabilities() []string {
	return []string{
		appservice.CapabilityID, appservice.QueryCapabilityID, appservice.MutationCapabilityID,
		appservice.RelatedCountCapabilityID, appservice.RecycleBinCapabilityID,
		appservice.RecycleBinMutationCapabilityID,
		appservice.RecordViewCapabilityID, appservice.RecordViewAttachmentSourceCapabilityID,
		appservice.FilesCapabilityID,
		appservice.BulkIngestCapabilityID,
		appservice.RecordDefaultsCapabilityID,
		appservice.EffectivePermissionsCapabilityID,
		appservice.SearchCapabilityID,
		appservice.FlowActionsCapabilityID,
		appservice.AnalyticsReadCapabilityID,
		appservice.AuditLogReadCapabilityID,
		appservice.ApprovalLifecycleCapabilityID,
		appservice.ApprovalReadCapabilityID,
		appservice.ApprovalNotificationFlowsCapabilityID,
		appservice.PrivateListViewCapabilityID,
		appservice.PermissionAuthoringCapabilityID,
		appservice.MetadataAdminReadCapabilityID,
		appservice.LayoutAuthoringCapabilityID,
		appservice.UIAppsCapabilityID,
		appservice.ApplicationNavigationCapabilityID,
		appservice.SessionCapabilityID,
		appservice.GoogleLoginStateCapabilityID,
	}
}

type processConfig struct {
	httpAddress             string
	runtime                 runtimegrpc.RuntimeClientConfig
	requestTimeout          time.Duration
	startupTimeout          time.Duration
	shutdownTimeout         time.Duration
	readHeaderTimeout       time.Duration
	idleTimeout             time.Duration
	maxRequestBytes         int64
	publicOrigin            string
	organizationID          string
	sessionControlOrigins   []string
	googleClientID          string
	googleClientSecret      string
	googleTimeout           time.Duration
	loginMethods            []string
	passwordGrantEnabled    bool
	passwordBreakGlassUsers []string
	// requireFederatedLoginState is secure-by-default. Operators may set the
	// corresponding environment variable to false for one rolling-deploy
	// window so callbacks initiated by the pre-upgrade frontend can finish, and
	// must restore true after that window expires.
	requireFederatedLoginState bool
}

func loadConfig(lookup func(string) (string, bool)) (processConfig, error) {
	required := func(name string) (string, error) {
		value, ok := lookup(name)
		if !ok || strings.TrimSpace(value) == "" || strings.TrimSpace(value) != value {
			return "", fmt.Errorf("sfcrm AppService: %s is required", name)
		}
		return value, nil
	}
	parseDuration := func(name string) (time.Duration, error) {
		value, err := required(name)
		if err != nil {
			return 0, err
		}
		parsed, err := time.ParseDuration(value)
		if err != nil || parsed <= 0 {
			return 0, fmt.Errorf("sfcrm AppService: %s must be a positive duration", name)
		}
		return parsed, nil
	}
	address, err := required("SFCRM_HTTP_ADDRESS")
	if err != nil {
		return processConfig{}, err
	}
	target, err := required("SFCRM_RUNTIME_TARGET")
	if err != nil {
		return processConfig{}, err
	}
	installation, err := required("SFCRM_APP_INSTALLATION_ID")
	if err != nil {
		return processConfig{}, err
	}
	publicOrigin, err := required("SFCRM_PUBLIC_ORIGIN")
	if err != nil {
		return processConfig{}, err
	}
	organizationID, err := required("SFCRM_ORGANIZATION_ID")
	if err != nil {
		return processConfig{}, err
	}
	sessionControlOriginsText, err := required("SFCRM_SESSION_CONTROL_ORIGINS")
	if err != nil {
		return processConfig{}, err
	}
	sessionControlOrigins := splitNonEmpty(sessionControlOriginsText)
	if len(sessionControlOrigins) == 0 {
		return processConfig{}, errors.New("sfcrm AppService: SFCRM_SESSION_CONTROL_ORIGINS must contain at least one origin")
	}
	tokenFile, err := required("SFCRM_WORKLOAD_TOKEN_FILE")
	if err != nil {
		return processConfig{}, err
	}
	tokenInfo, err := os.Lstat(tokenFile)
	if err != nil {
		return processConfig{}, fmt.Errorf("sfcrm AppService: inspect workload token: %w", err)
	}
	if !tokenInfo.Mode().IsRegular() || tokenInfo.Mode().Perm()&0o077 != 0 {
		return processConfig{}, errors.New("sfcrm AppService: workload token must be a regular file unreadable by group and others")
	}
	tokenBytes, err := os.ReadFile(tokenFile)
	if err != nil {
		return processConfig{}, fmt.Errorf("sfcrm AppService: read workload token: %w", err)
	}
	workloadToken := strings.TrimSuffix(strings.TrimSuffix(string(tokenBytes), "\n"), "\r")
	if workloadToken == "" || strings.TrimSpace(workloadToken) != workloadToken || strings.ContainsAny(workloadToken, "\r\n") {
		return processConfig{}, errors.New("sfcrm AppService: workload token file must contain one non-empty token")
	}
	durationNames := []string{
		"SFCRM_REQUEST_TIMEOUT", "SFCRM_RUNTIME_ATTEMPT_TIMEOUT", "SFCRM_RUNTIME_STARTUP_TIMEOUT", "SFCRM_SHUTDOWN_TIMEOUT",
		"SFCRM_READ_HEADER_TIMEOUT", "SFCRM_IDLE_TIMEOUT", "SFCRM_GOOGLE_TIMEOUT",
	}
	durations := make(map[string]time.Duration, len(durationNames))
	for _, name := range durationNames {
		durations[name], err = parseDuration(name)
		if err != nil {
			return processConfig{}, err
		}
	}
	bodyLimitText, err := required("SFCRM_MAX_REQUEST_BODY_BYTES")
	if err != nil {
		return processConfig{}, err
	}
	bodyLimit, err := strconv.ParseInt(bodyLimitText, 10, 64)
	if err != nil || bodyLimit < 26*1024*1024 {
		return processConfig{}, errors.New("sfcrm AppService: SFCRM_MAX_REQUEST_BODY_BYTES must allow the 25 MiB record-file contract plus multipart framing")
	}
	value := func(name string) string {
		result, _ := lookup(name)
		return result
	}
	googleClientID, googleClientSecret := value("GOOGLE_CLIENT_ID"), value("GOOGLE_CLIENT_SECRET")
	if (googleClientID == "") != (googleClientSecret == "") || strings.TrimSpace(googleClientID) != googleClientID || strings.TrimSpace(googleClientSecret) != googleClientSecret {
		return processConfig{}, errors.New("sfcrm AppService: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be configured together without surrounding whitespace")
	}
	requireFederatedLoginState, err := optionalStrictBool(lookup, "SFCRM_REQUIRE_FEDERATED_LOGIN_STATE", true)
	if err != nil {
		return processConfig{}, err
	}
	return processConfig{
		httpAddress: address, requestTimeout: durations["SFCRM_REQUEST_TIMEOUT"],
		startupTimeout: durations["SFCRM_RUNTIME_STARTUP_TIMEOUT"], shutdownTimeout: durations["SFCRM_SHUTDOWN_TIMEOUT"],
		readHeaderTimeout: durations["SFCRM_READ_HEADER_TIMEOUT"], idleTimeout: durations["SFCRM_IDLE_TIMEOUT"],
		maxRequestBytes: bodyLimit,
		publicOrigin:    publicOrigin, organizationID: organizationID,
		sessionControlOrigins: sessionControlOrigins,
		googleClientID:        googleClientID, googleClientSecret: googleClientSecret,
		googleTimeout:              durations["SFCRM_GOOGLE_TIMEOUT"],
		loginMethods:               splitNonEmpty(value("NEXUS_LOGIN_METHODS")),
		passwordGrantEnabled:       passwordGrantEnabled(value("NEXUS_PASSWORD_GRANT_ENABLED")),
		passwordBreakGlassUsers:    splitNonEmpty(value("NEXUS_PASSWORD_GRANT_BREAKGLASS")),
		requireFederatedLoginState: requireFederatedLoginState,
		// L1 selects the neutral endpoint and workload identity, not an L0
		// implementation release. Explicit RPC/capability requirements own API
		// compatibility; an absent API version must not create a module, source,
		// repository, or unified-L0-version gate.
		runtime: runtimegrpc.RuntimeClientConfig{
			Target: target, AppInstallationID: installation, WorkloadToken: workloadToken,
			AttemptTimeout: durations["SFCRM_RUNTIME_ATTEMPT_TIMEOUT"],
			CAFile:         value("SFCRM_RUNTIME_CA_FILE"), ServerName: value("SFCRM_RUNTIME_SERVER_NAME"),
			ClientCertificateFile: value("SFCRM_RUNTIME_CLIENT_CERT_FILE"), ClientKeyFile: value("SFCRM_RUNTIME_CLIENT_KEY_FILE"),
		},
	}, nil
}

func optionalStrictBool(lookup func(string) (string, bool), name string, defaultValue bool) (bool, error) {
	raw, ok := lookup(name)
	if !ok || raw == "" {
		return defaultValue, nil
	}
	if strings.TrimSpace(raw) != raw {
		return false, fmt.Errorf("sfcrm AppService: %s must be true or false without surrounding whitespace", name)
	}
	switch strings.ToLower(raw) {
	case "true":
		return true, nil
	case "false":
		return false, nil
	default:
		return false, fmt.Errorf("sfcrm AppService: %s must be true or false", name)
	}
}

func splitNonEmpty(raw string) []string {
	values := make([]string, 0)
	for _, part := range strings.Split(raw, ",") {
		if value := strings.TrimSpace(part); value != "" {
			values = append(values, value)
		}
	}
	return values
}

func passwordGrantEnabled(raw string) bool {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "0", "f", "false", "n", "no", "off", "disable", "disabled":
		return false
	default:
		return true
	}
}
