package sfcrmappservice

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	navigationsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/applicationnavigationv1"
	"net/http"
	"strings"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
)

const (
	applicationNavigationKind = catalogsemantic.EffectiveApplicationNavigationComponentKind
	applicationNavigationKey  = catalogsemantic.EffectiveApplicationNavigationComponentBusinessKey
)

// RuntimeUIAppsClient combines immutable application shell definitions with
// current Runtime navigation. Catalog never supplies editable navigation.
type RuntimeUIAppsClient struct {
	resolveNavigation    RuntimeResolveApplicationNavigationFunc
	appMetadata          RuntimeGetAppMetadataViewV3Func
	readEffective        RuntimeReadEffectivePermissionsFunc
	reconcileAppMetadata RuntimeReconcileAppMetadataV3ReceiptsFunc
}

func NewRuntimeUIAppsClient(
	appMetadata RuntimeGetAppMetadataViewV3Func,
	readEffective RuntimeReadEffectivePermissionsFunc,
	resolveNavigation RuntimeResolveApplicationNavigationFunc,
) (*RuntimeUIAppsClient, error) {
	if appMetadata == nil || readEffective == nil || resolveNavigation == nil {
		return nil, errors.New("sfcrm AppService: application definitions, current navigation and effective permissions are required for UI Apps")
	}
	return &RuntimeUIAppsClient{appMetadata: appMetadata, readEffective: readEffective, resolveNavigation: resolveNavigation}, nil
}

func matchUIAppsRoute(escapedPath string) bool {
	parts := strings.Split(strings.Trim(escapedPath, "/"), "/")
	if len(parts) != 5 ||
		parts[0] != "services" ||
		parts[1] != "data" ||
		parts[3] != "ui-api" ||
		parts[4] != "apps" {
		return false
	}
	_, supported := recordViewVersions[parts[2]]
	return supported
}

func (service *Service) serveUIApps(writer http.ResponseWriter, request *http.Request) bool {
	definitionsOnly := matchApplicationDefinitionsRoute(request.URL.EscapedPath())
	if !definitionsOnly && !matchUIAppsRoute(request.URL.EscapedPath()) {
		return false
	}
	if request.Method != http.MethodGet {
		writer.Header().Set("Allow", http.MethodGet)
		writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
		return true
	}
	service.requests.Add(1)
	if service.draining.Load() {
		service.drainRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return true
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return true
	}
	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()
	sections := []permissionsemantic.Section{permissionsemantic.SectionApplicationPermissions, permissionsemantic.SectionNavigationPermissions}
	if definitionsOnly {
		sections = []permissionsemantic.Section{permissionsemantic.SectionSystemPermissions}
	}
	view, permissions, err := service.uiApps.authority(ctx, bearer, sections...)
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return true
	}
	if definitionsOnly && !toolingQueryPermissionGranted(permissions.SystemPermissions) {
		service.failures.Add(1)
		writeRuntimeFailure(writer, &commonsemantic.Failure{Category: commonsemantic.FailureForbidden})
		return true
	}
	definitions, err := decodeApplicationDefinitions(view)
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return true
	}
	if definitionsOnly {
		result, err := projectApplicationDefinitions(definitions)
		if err != nil {
			service.failures.Add(1)
			writeRuntimeFailure(writer, err)
			return true
		}
		service.successes.Add(1)
		writeJSON(writer, http.StatusOK, map[string]any{"apps": result})
		return true
	}
	ids := make([]string, 0, len(definitions.Applications))
	for _, app := range definitions.Applications {
		ids = append(ids, app.ID)
	}
	navigationRequest := navigationsemantic.ResolveRequest{ApplicationIDs: ids}
	navigation, err := service.uiApps.resolveNavigation(ctx, bearer, navigationRequest)
	if err == nil {
		err = navigationsemantic.ValidateResolveResponse(navigationRequest, navigation)
	}
	if err == nil && navigation.Receipts.SecurityEpoch != permissions.Receipts.SecurityEpoch {
		err = unavailableUIApps()
	}
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return true
	}
	result, err := projectUIApplications(view, permissions, navigation.Documents)
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return true
	}
	service.successes.Add(1)
	writeJSON(writer, http.StatusOK, result)
	return true
}

func (client *RuntimeUIAppsClient) authority(
	ctx context.Context,
	bearer string,
	sections ...permissionsemantic.Section,
) (catalogsemantic.AppMetadataViewV3, permissionsemantic.Response, error) {
	if client == nil || client.appMetadata == nil || client.readEffective == nil {
		return catalogsemantic.AppMetadataViewV3{}, permissionsemantic.Response{}, unavailableUIApps()
	}
	view, err := client.appMetadata(ctx, catalogsemantic.GetRequest{})
	if err != nil {
		return catalogsemantic.AppMetadataViewV3{}, permissionsemantic.Response{}, normalizeRuntimeError(err)
	}
	if err := catalogsemantic.ValidateAppMetadataViewV3(view); err != nil || view.AppID != AppID {
		return catalogsemantic.AppMetadataViewV3{}, permissionsemantic.Response{}, unavailableUIApps()
	}
	revision := view.Receipts.CatalogRevision
	permissions, err := client.readEffective(ctx, bearer, permissionsemantic.Request{
		MinCatalogRevision: &revision,
		Selection:          &permissionsemantic.Selection{Sections: sections},
	})
	if err != nil {
		return catalogsemantic.AppMetadataViewV3{}, permissionsemantic.Response{}, normalizeRuntimeError(err)
	}
	if err := permissionsemantic.ValidateResponse(permissions); err != nil {
		return catalogsemantic.AppMetadataViewV3{}, permissionsemantic.Response{}, unavailableUIApps()
	}
	if err := sameUIAppsAuthority(
		ctx, client.reconcileAppMetadata, view.Receipts, permissions.Receipts,
	); err != nil {
		return catalogsemantic.AppMetadataViewV3{}, permissionsemantic.Response{}, err
	}
	return view, permissions, nil
}

// Installed application definitions belong to Catalog. This administration read
// deliberately neither resolves navigation nor advertises shell mutation support.
// Navigation continues to have exactly one current read/write owner in Runtime.
func matchApplicationDefinitionsRoute(path string) bool {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) != 5 || parts[0] != "services" || parts[1] != "data" || parts[3] != "metadata" || parts[4] != "applications" {
		return false
	}
	_, supported := recordViewVersions[parts[2]]
	return supported
}

type applicationDefinition struct {
	ID            string `json:"id"`
	FrontendAppID string `json:"frontendAppId,omitempty"`
	Label         string `json:"label"`
	Description   string `json:"description,omitempty"`
	Icon          string `json:"icon,omitempty"`
	Color         string `json:"color,omitempty"`
}

func projectApplicationDefinitions(document effectiveNavigationDocument) ([]applicationDefinition, error) {
	result := make([]applicationDefinition, 0, len(document.Applications))
	seen := make(map[string]bool, len(document.Applications))
	for _, app := range document.Applications {
		key := strings.ToLower(app.ID)
		if !validCanonicalText(app.ID) || seen[key] || !validOptionalCanonicalText(app.FrontendAppID) ||
			!validOptionalCanonicalText(app.Label) || !validOptionalCanonicalText(app.Description) ||
			!validOptionalCanonicalText(app.Icon) || !validOptionalCanonicalText(app.Color) {
			return nil, unavailableUIApps()
		}
		seen[key] = true
		result = append(result, applicationDefinition{ID: app.ID, FrontendAppID: app.FrontendAppID, Label: app.Label, Description: app.Description, Icon: app.Icon, Color: app.Color})
	}
	return result, nil
}

type effectiveNavigationDocument struct {
	Applications []effectiveNavigationApplication `json:"applications"`
}

type effectiveNavigationApplication struct {
	ID              string                    `json:"id"`
	FrontendAppID   string                    `json:"frontendAppId,omitempty"`
	Label           string                    `json:"label"`
	Description     string                    `json:"description,omitempty"`
	Icon            string                    `json:"icon,omitempty"`
	Color           string                    `json:"color,omitempty"`
	NavigationItems []effectiveNavigationItem `json:"navigationItems"`
	UtilityItems    []effectiveUtilityItem    `json:"utilityItems"`
}

type effectiveNavigationItem struct {
	ID            string `json:"id"`
	Type          string `json:"type"`
	ObjectAPIName string `json:"objectApiName,omitempty"`
	PageURL       string `json:"pageUrl,omitempty"`
	DashboardID   string `json:"dashboardId,omitempty"`
	Label         string `json:"label"`
	Icon          string `json:"icon,omitempty"`
}

type effectiveUtilityItem struct {
	ID          string `json:"id"`
	Type        string `json:"type"`
	Label       string `json:"label"`
	Icon        string `json:"icon,omitempty"`
	PanelWidth  *int   `json:"panelWidth,omitempty"`
	PanelHeight *int   `json:"panelHeight,omitempty"`
}

type uiAppPageReference struct {
	Type       string            `json:"type"`
	Attributes map[string]string `json:"attributes,omitempty"`
}

type uiAppNavigationItem struct {
	ID            string              `json:"id"`
	DeveloperName string              `json:"developerName"`
	Label         string              `json:"label"`
	ObjectAPIName string              `json:"objectApiName,omitempty"`
	IconURL       string              `json:"iconUrl,omitempty"`
	StandardType  string              `json:"standardType,omitempty"`
	ItemType      string              `json:"itemType,omitempty"`
	PageReference *uiAppPageReference `json:"pageReference,omitempty"`
}

type uiApp struct {
	AppID                        string                 `json:"appId"`
	FrontendAppID                string                 `json:"frontendAppId,omitempty"`
	DeveloperName                string                 `json:"developerName,omitempty"`
	Description                  string                 `json:"description,omitempty"`
	FormFactors                  []string               `json:"formFactors"`
	HeaderColor                  string                 `json:"headerColor,omitempty"`
	IconURL                      string                 `json:"iconUrl,omitempty"`
	IsNavPersonalizationDisabled bool                   `json:"isNavPersonalizationDisabled"`
	Label                        string                 `json:"label"`
	LogoURL                      string                 `json:"logoUrl,omitempty"`
	NavItems                     []uiAppNavigationItem  `json:"navItems"`
	Selected                     bool                   `json:"selected"`
	UtilityItems                 []effectiveUtilityItem `json:"utilityItems,omitempty"`
}

func projectUIApplications(
	view catalogsemantic.AppMetadataViewV3,
	permissions permissionsemantic.Response,
	current []navigationsemantic.Document,
) (map[string]any, error) {
	document, err := decodeApplicationDefinitions(view)
	if err != nil {
		return nil, err
	}
	currentByID := make(map[string][]effectiveNavigationItem, len(current))
	for _, navigation := range current {
		items, err := projectCurrentNavigation(navigation.Items)
		if err != nil {
			return nil, err
		}
		currentByID[navigation.ApplicationID] = items
	}
	applicationPermissions := make(map[string]permissionsemantic.ApplicationPermission, len(permissions.ApplicationPermissions))
	for _, permission := range permissions.ApplicationPermissions {
		applicationPermissions[strings.ToLower(permission.ApplicationID)] = permission
	}
	apps := make(map[string]uiApp)
	seen := make(map[string]struct{}, len(document.Applications))
	for _, application := range document.Applications {
		// Ignore historical navigation even when the same Catalog revision remains
		// active for the entire editing session. Missing current documents stay hidden.
		items, present := currentByID[application.ID]
		application.NavigationItems = items
		if !present {
			application.NavigationItems = []effectiveNavigationItem{}
		}
		if !validEffectiveNavigationApplication(application) {
			return nil, unavailableUIApps()
		}
		key := strings.ToLower(application.ID)
		if _, duplicate := seen[key]; duplicate {
			return nil, unavailableUIApps()
		}
		seen[key] = struct{}{}
		permission, present := applicationPermissions[key]
		if _, current := currentByID[application.ID]; !current || !present || !permission.CanView {
			continue
		}
		projected := uiApp{
			AppID: application.ID, FrontendAppID: application.FrontendAppID,
			DeveloperName: application.ID, Description: application.Description,
			FormFactors: []string{"Large"}, HeaderColor: application.Color,
			IconURL: application.Icon, Label: application.Label,
			NavItems:     make([]uiAppNavigationItem, 0, len(application.NavigationItems)),
			Selected:     permission.IsDefault,
			UtilityItems: append([]effectiveUtilityItem(nil), application.UtilityItems...),
		}
		for _, item := range application.NavigationItems {
			projected.NavItems = append(projected.NavItems, projectNavigationItem(item))
		}
		apps[application.ID] = projected
	}
	return map[string]any{"apps": apps}, nil
}

func validEffectiveNavigationApplication(application effectiveNavigationApplication) bool {
	if !validCanonicalText(application.ID) || !validOptionalCanonicalText(application.Label) ||
		application.NavigationItems == nil || application.UtilityItems == nil ||
		!validOptionalCanonicalText(application.FrontendAppID) ||
		!validOptionalCanonicalText(application.Description) ||
		!validOptionalCanonicalText(application.Icon) ||
		!validOptionalCanonicalText(application.Color) {
		return false
	}
	navigationSeen := make(map[string]struct{}, len(application.NavigationItems))
	for _, item := range application.NavigationItems {
		key := strings.ToLower(item.ID)
		if !validNavigationItem(item) {
			return false
		}
		if _, duplicate := navigationSeen[key]; duplicate {
			return false
		}
		navigationSeen[key] = struct{}{}
	}
	utilitySeen := make(map[string]struct{}, len(application.UtilityItems))
	for _, item := range application.UtilityItems {
		key := strings.ToLower(item.ID)
		if !validCanonicalText(item.ID) || !validCanonicalText(item.Type) ||
			!validOptionalCanonicalText(item.Label) || !validOptionalCanonicalText(item.Icon) {
			return false
		}
		if _, duplicate := utilitySeen[key]; duplicate {
			return false
		}
		utilitySeen[key] = struct{}{}
	}
	return true
}

func effectiveNavigationComponent(view catalogsemantic.AppMetadataViewV3) (catalogsemantic.EffectiveMetadataComponent, error) {
	var selected catalogsemantic.EffectiveMetadataComponent
	count := 0
	for _, component := range view.EffectiveComponents {
		if component.ComponentKind == applicationNavigationKind && component.BusinessKey == applicationNavigationKey {
			selected = component
			count++
		}
	}
	if count != 1 || selected.ComponentFormatVersion != 1 || len(selected.CanonicalBehavior) == 0 {
		return catalogsemantic.EffectiveMetadataComponent{}, unavailableUIApps()
	}
	return selected, nil
}

func validNavigationItem(item effectiveNavigationItem) bool {
	if !validCanonicalText(item.ID) || !validOptionalCanonicalText(item.Label) {
		return false
	}
	switch item.Type {
	case "object":
		return validCanonicalText(item.ObjectAPIName)
	case "page":
		return true
	case "web":
		return validCanonicalText(item.PageURL)
	case "dashboard":
		return validCanonicalText(item.DashboardID)
	default:
		return false
	}
}

func projectNavigationItem(item effectiveNavigationItem) uiAppNavigationItem {
	result := uiAppNavigationItem{
		ID: item.ID, DeveloperName: item.ID, Label: item.Label,
		IconURL: item.Icon, ItemType: item.Type,
	}
	switch item.Type {
	case "object":
		result.ObjectAPIName = item.ObjectAPIName
	case "page":
		result.StandardType = item.ObjectAPIName
	case "web":
		result.PageReference = &uiAppPageReference{Type: "standard__webPage", Attributes: map[string]string{"url": item.PageURL}}
	case "dashboard":
		result.PageReference = &uiAppPageReference{Type: "standard__dashboard", Attributes: map[string]string{"dashboardId": item.DashboardID}}
	}
	return result
}

func sameUIAppsAuthority(
	ctx context.Context,
	reconcile RuntimeReconcileAppMetadataV3ReceiptsFunc,
	expected commonsemantic.Receipts,
	actual commonsemantic.Receipts,
) error {
	if expected.CatalogRevision != actual.CatalogRevision {
		return &commonsemantic.Failure{Category: commonsemantic.FailureStaleState, StaleAxis: commonsemantic.StaleAxisCatalog, RequiredRevision: expected.CatalogRevision, ServedRevision: actual.CatalogRevision}
	}
	if expected.SecurityEpoch != actual.SecurityEpoch {
		reconciled, err := reconcileAppMetadataSecurityEpoch(
			ctx, reconcile, expected, actual,
		)
		if err != nil {
			return err
		}
		if reconciled {
			return nil
		}
		return &commonsemantic.Failure{Category: commonsemantic.FailureStaleState, StaleAxis: commonsemantic.StaleAxisSecurity, RequiredRevision: expected.SecurityEpoch, ServedRevision: actual.SecurityEpoch}
	}
	if expected.SchemaGeneration != actual.SchemaGeneration || expected.EngineGeneration != actual.EngineGeneration {
		return unavailableUIApps()
	}
	return nil
}

func unavailableUIApps() error {
	return &commonsemantic.Failure{Category: commonsemantic.FailureInternal}
}

func validCanonicalText(value string) bool {
	return value != "" && value == strings.TrimSpace(value)
}

func validOptionalCanonicalText(value string) bool {
	return value == "" || value == strings.TrimSpace(value)
}

func decodeApplicationDefinitions(view catalogsemantic.AppMetadataViewV3) (effectiveNavigationDocument, error) {
	component, err := effectiveNavigationComponent(view)
	if err != nil {
		return effectiveNavigationDocument{}, err
	}
	var document effectiveNavigationDocument
	decoder := json.NewDecoder(bytes.NewReader(component.CanonicalBehavior))
	decoder.DisallowUnknownFields()
	if decoder.Decode(&document) != nil || document.Applications == nil || requireJSONEOF(decoder) != nil {
		return document, unavailableUIApps()
	}
	return document, nil
}
func projectCurrentNavigation(items []navigationsemantic.Item) ([]effectiveNavigationItem, error) {
	pages, err := registeredNavigationPages()
	if err != nil {
		return nil, unavailableUIApps()
	}
	result := make([]effectiveNavigationItem, 0, len(items))
	for _, item := range items {
		value := effectiveNavigationItem{ID: item.ID, Label: item.Label, Icon: item.Icon}
		switch {
		case item.Target.URL != nil:
			value.Type = "web"
			value.PageURL = *item.Target.URL
		case item.Target.Page != nil:
			if _, available := pages[item.Target.Page.ID]; !available {
				// Keep the reference in the full editor document; this release
				// cannot serve it. Save/replay never consult this projection.
				continue
			}
			value.Type = "page"
			value.ObjectAPIName = item.Target.Page.ID
		case item.Target.Resource != nil:
			resource := item.Target.Resource
			if resource.ResourceID == "" {
				value.Type = "object"
				value.ObjectAPIName = resource.ResourceType
			} else if resource.ResourceType == "Dashboard" {
				value.Type = "dashboard"
				value.DashboardID = resource.ResourceID
			} else {
				return nil, unavailableUIApps()
			}
		default:
			return nil, unavailableUIApps()
		}
		result = append(result, value)
	}
	return result, nil
}
