package dialectsfcrm

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
)

type sfUIAppNavItem 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 *sfUIAppPageReference `json:"pageReference,omitempty"`
}

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

type sfUIApp 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                     []sfUIAppNavItem     `json:"navItems"`
	Selected                     bool                 `json:"selected"`
	UtilityItems                 []models.UtilityItem `json:"utilityItems,omitempty"`
}

type sfUIAppsResponse struct {
	Apps map[string]sfUIApp `json:"apps"`
}

type toolingCustomApplicationMetadata struct {
	Label         string   `json:"label"`
	Description   string   `json:"description,omitempty"`
	FrontendAppId string   `json:"frontendAppId,omitempty"`
	NavType       string   `json:"navType,omitempty"`
	UiType        string   `json:"uiType,omitempty"`
	FormFactors   []string `json:"formFactors,omitempty"`
	Tabs          []string `json:"tabs,omitempty"`
}

type toolingCustomApplicationRequest struct {
	FullName string                           `json:"FullName"`
	Metadata toolingCustomApplicationMetadata `json:"Metadata"`
}

var standardPageTabs = map[string]string{
	"home":              "Home",
	"chatter":           "Chatter",
	"wavehomelightning": "WaveHomeLightning",
}

const applicationNavigationGenericInternalErrorMessage = "An internal server error occurred. Contact your system administrator."

func ListUIApplicationsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	if req.User == nil {
		return &dialect.Response{Status: http.StatusUnauthorized, Body: wire.SFErrorResponse(wire.SFCodeInvalidSessionID, "Authentication required")}, nil
	}
	apps, resp := visibleApplications(ctx, req, deps)
	if resp != nil {
		return resp, nil
	}
	if resp := enrichApplicationNavigationLabels(ctx, deps, apps); resp != nil {
		return resp, nil
	}

	sfApps := make(map[string]sfUIApp, len(apps))
	for _, app := range apps {
		sf := toSFUIApp(app)
		sfApps[sf.AppId] = sf
	}
	return jsonResponse(http.StatusOK, sfUIAppsResponse{Apps: sfApps}), nil
}

func ToolingCustomApplicationListNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	apps, resp := applications(ctx, req, deps)
	if resp != nil {
		return resp, nil
	}
	sfApps := make([]sfUIApp, 0, len(apps))
	for _, app := range apps {
		if app == nil {
			continue
		}
		sfApps = append(sfApps, toSFUIApp(app))
	}
	return jsonResponse(http.StatusOK, map[string]any{
		"records":   sfApps,
		"done":      true,
		"totalSize": len(sfApps),
	}), nil
}

func ToolingCustomApplicationGetNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	appNav, resp := applicationNavigationFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	app, err := appNav.GetApplication(ctx, req.Params["id"])
	if err != nil {
		return applicationNavigationErrorResponse(err), nil
	}
	if app == nil {
		return &dialect.Response{Status: http.StatusNotFound, Body: wire.SFErrorResponse(wire.SFCodeNotFound, "Application not found")}, nil
	}

	tabs := make([]string, 0, len(app.NavigationItems))
	for _, nav := range app.NavigationItems {
		tabs = append(tabs, navigationItemToolingTab(nav))
	}
	return jsonResponse(http.StatusOK, map[string]any{
		"Id":       app.ID,
		"FullName": app.ID,
		"Metadata": map[string]any{
			"label":         app.Label,
			"description":   app.Description,
			"frontendAppId": app.FrontendAppID,
			"tabs":          tabs,
		},
	}), nil
}

func ToolingCustomApplicationCreateNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	appNav, resp := applicationNavigationFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	body, resp := decodeToolingCustomApplicationRequest(req)
	if resp != nil {
		return resp, nil
	}
	app := body.toAppConfig()
	if app.NavigationItems == nil {
		app.NavigationItems = []models.NavigationItem{}
	}
	if resp := enrichApplicationNavigationLabels(ctx, deps, []*models.AppConfig{app}); resp != nil {
		return resp, nil
	}
	if err := appNav.CreateApplication(ctx, app); err != nil {
		return applicationNavigationErrorResponse(err), nil
	}
	return toolingApplicationSuccess(http.StatusCreated, app.ID), nil
}

func ToolingCustomApplicationUpdateNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	appNav, resp := applicationNavigationFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	body, resp := decodeToolingCustomApplicationRequest(req)
	if resp != nil {
		return resp, nil
	}
	id := req.Params["id"]
	updates := body.toAppConfig()
	updates.ID = id
	if updates.NavigationItems != nil {
		if resp := enrichApplicationNavigationLabels(ctx, deps, []*models.AppConfig{updates}); resp != nil {
			return resp, nil
		}
	}
	presence := contract.ApplicationPatchPresence{
		NavigationItemIdentities: toolingTabNavigationIdentities(body.Metadata.Tabs, updates.NavigationItems),
	}
	if err := appNav.UpdateApplication(ctx, id, updates, presence); err != nil {
		return applicationNavigationErrorResponse(err), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

func ToolingCustomApplicationDeleteNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	appNav, resp := applicationNavigationFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	if err := appNav.DeleteApplication(ctx, req.Params["id"]); err != nil {
		return applicationNavigationErrorResponse(err), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

func decodeToolingCustomApplicationRequest(req *dialect.Request) (*toolingCustomApplicationRequest, *dialect.Response) {
	var body toolingCustomApplicationRequest
	if err := json.NewDecoder(bytes.NewReader(req.Body)).Decode(&body); err != nil {
		return nil, &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, err.Error())}
	}
	return &body, nil
}

func (r *toolingCustomApplicationRequest) toAppConfig() *models.AppConfig {
	app := &models.AppConfig{
		ID:            r.FullName,
		FrontendAppID: r.Metadata.FrontendAppId,
		Label:         r.Metadata.Label,
		Description:   r.Metadata.Description,
	}
	if r.Metadata.Tabs != nil {
		navItems := make([]models.NavigationItem, 0, len(r.Metadata.Tabs))
		for i, tab := range r.Metadata.Tabs {
			navItems = append(navItems, toolingTabToNavigationItem(tab, i))
		}
		app.NavigationItems = navItems
	}
	return app
}

func toolingTabToNavigationItem(tab string, index int) models.NavigationItem {
	item := models.NavigationItem{ID: fmt.Sprintf("nav-%d-%s", index, tab)}
	if strings.HasPrefix(tab, "standard-") {
		suffix := strings.TrimPrefix(tab, "standard-")
		if pageName, ok := standardPageTabs[strings.ToLower(suffix)]; ok {
			item.Type = constants.NavigationTypePage
			item.ObjectAPIName = pageName
			item.Label = pageName
			return item
		}
		item.Type = constants.NavigationTypeObject
		item.ObjectAPIName = suffix
		item.Label = suffix
		return item
	}
	if strings.HasSuffix(tab, "__c") {
		item.Type = constants.NavigationTypeObject
		item.ObjectAPIName = tab
		item.Label = tab
		return item
	}
	item.Type = constants.NavigationTypePage
	item.Label = tab
	return item
}

func toSFUIApp(app *models.AppConfig) sfUIApp {
	navItems := make([]sfUIAppNavItem, 0, len(app.NavigationItems))
	for _, nav := range app.NavigationItems {
		item := sfUIAppNavItem{
			ID:            nav.ID,
			DeveloperName: nav.ID,
			Label:         nav.Label,
			IconUrl:       nav.Icon,
			ItemType:      nav.Type,
		}
		switch nav.Type {
		case constants.NavigationTypeObject:
			item.ObjectApiName = nav.ObjectAPIName
		case constants.NavigationTypeWeb:
			item.PageReference = &sfUIAppPageReference{
				Type:       "standard__webPage",
				Attributes: map[string]string{"url": nav.PageURL},
			}
		case constants.NavigationTypeDashboard:
			item.PageReference = &sfUIAppPageReference{
				Type:       "standard__dashboard",
				Attributes: map[string]string{"dashboardId": nav.DashboardID},
			}
		case constants.NavigationTypePage:
			item.StandardType = nav.ObjectAPIName
		}
		navItems = append(navItems, item)
	}
	return sfUIApp{
		AppId:         app.ID,
		FrontendAppId: app.FrontendAppID,
		DeveloperName: app.ID,
		Label:         app.Label,
		Description:   app.Description,
		FormFactors:   []string{"Large"},
		HeaderColor:   app.Color,
		IconUrl:       app.Icon,
		NavItems:      navItems,
		Selected:      app.IsDefault,
		UtilityItems:  app.UtilityItems,
	}
}

func applications(ctx context.Context, req *dialect.Request, deps dialect.Deps) ([]*models.AppConfig, *dialect.Response) {
	appNav, resp := applicationNavigationFromDeps(deps)
	if resp != nil {
		return nil, resp
	}
	apps, err := appNav.ListApplications(ctx)
	if err != nil {
		return nil, applicationNavigationErrorResponse(err)
	}
	return apps, nil
}

func visibleApplications(ctx context.Context, req *dialect.Request, deps dialect.Deps) ([]*models.AppConfig, *dialect.Response) {
	appNav, resp := applicationNavigationFromDeps(deps)
	if resp != nil {
		return nil, resp
	}
	apps, err := appNav.ListVisibleApplications(ctx, req.User)
	if err != nil {
		return nil, applicationNavigationErrorResponse(err)
	}
	return apps, nil
}

func applicationNavigationFromDeps(deps dialect.Deps) (contract.ApplicationNavigation, *dialect.Response) {
	if deps.Contracts.ApplicationNavigation == nil {
		logApplicationNavigationInternalError("application navigation contract unavailable", nil)
		return nil, applicationNavigationInternalErrorResponse()
	}
	return deps.Contracts.ApplicationNavigation, nil
}

func enrichApplicationNavigationLabels(ctx context.Context, deps dialect.Deps, apps []*models.AppConfig) *dialect.Response {
	if len(apps) == 0 {
		return nil
	}
	if deps.Contracts.Metadata == nil {
		logApplicationNavigationInternalError("metadata contract unavailable during application navigation label enrichment", nil)
		return applicationNavigationInternalErrorResponse()
	}
	schemas, err := deps.Contracts.Metadata.ListObjects(ctx)
	if err != nil {
		logApplicationNavigationInternalError("metadata list objects failed during application navigation label enrichment", err)
		return applicationNavigationInternalErrorResponse()
	}
	labelMap := make(map[string]*models.ObjectMetadata, len(schemas))
	for i := range schemas {
		schema := &schemas[i]
		labelMap[strings.ToLower(schema.APIName)] = schema
	}
	for _, app := range apps {
		enrichApplicationNavigationLabelsFromSchemas(app, labelMap)
	}
	return nil
}

func enrichApplicationNavigationLabelsFromSchemas(app *models.AppConfig, labelMap map[string]*models.ObjectMetadata) {
	if app == nil || len(app.NavigationItems) == 0 {
		return
	}
	for i := range app.NavigationItems {
		item := &app.NavigationItems[i]
		if item.Type != constants.NavigationTypeObject || item.ObjectAPIName == "" {
			continue
		}
		schema, ok := labelMap[strings.ToLower(item.ObjectAPIName)]
		if !ok {
			continue
		}
		item.ObjectAPIName = schema.APIName
		if schema.PluralLabel != "" {
			item.Label = schema.PluralLabel
		} else if schema.Label != "" {
			item.Label = schema.Label
		}
	}
}

func toolingTabNavigationIdentities(tabs []string, items []models.NavigationItem) []contract.ApplicationNavigationItemIdentity {
	if tabs == nil {
		return nil
	}
	identities := make([]contract.ApplicationNavigationItemIdentity, 0, len(tabs))
	for i, tab := range tabs {
		var item models.NavigationItem
		if i < len(items) {
			item = items[i]
		}
		identities = append(identities, toolingTabNavigationIdentity(tab, item))
	}
	return identities
}

func toolingTabNavigationIdentity(tab string, item models.NavigationItem) contract.ApplicationNavigationItemIdentity {
	trimmed := strings.TrimSpace(tab)
	lowered := strings.ToLower(trimmed)
	if strings.HasPrefix(lowered, "standard-") {
		suffix := strings.TrimPrefix(trimmed, "standard-")
		if pageName, ok := standardPageTabs[strings.ToLower(suffix)]; ok {
			return contract.ApplicationNavigationItemIdentity{
				Kind: contract.ApplicationNavigationItemIdentityPage,
				Key:  pageName,
			}
		}
		return contract.ApplicationNavigationItemIdentity{
			Kind: contract.ApplicationNavigationItemIdentityObject,
			Key:  item.ObjectAPIName,
		}
	}
	if item.Type == constants.NavigationTypeObject {
		return contract.ApplicationNavigationItemIdentity{
			Kind: contract.ApplicationNavigationItemIdentityObject,
			Key:  item.ObjectAPIName,
		}
	}
	return contract.ApplicationNavigationItemIdentity{
		Kind: contract.ApplicationNavigationItemIdentityID,
		Key:  trimmed,
	}
}

func navigationItemToolingTab(item models.NavigationItem) string {
	switch {
	case item.Type == constants.NavigationTypeObject && strings.HasSuffix(item.ObjectAPIName, "__c"):
		return item.ObjectAPIName
	case item.Type == constants.NavigationTypeObject:
		return "standard-" + strings.ToLower(strings.TrimSpace(item.ObjectAPIName))
	case item.Type == constants.NavigationTypePage:
		tabName, ok := item.CanonicalTabName()
		if ok {
			return "standard-" + strings.ToLower(tabName)
		}
		if item.ID != "" {
			return item.ID
		}
	default:
		if item.ID != "" {
			return item.ID
		}
	}
	return ""
}

func applicationNavigationErrorResponse(err error) *dialect.Response {
	if err == nil {
		return nil
	}
	var appErr *contract.ApplicationNavigationError
	if !errors.As(err, &appErr) {
		logApplicationNavigationInternalError("application navigation contract returned internal error", err)
		return applicationNavigationInternalErrorResponse()
	}
	message := strings.TrimSpace(appErr.Message)
	if message == "" {
		message = appErr.Error()
	}
	switch appErr.Kind {
	case contract.ApplicationNavigationErrorValidation:
		if appErr.Field != "" {
			return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorWithFields(wire.SFCodeInvalidField, message, appErr.Field)}
		}
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidField, message)}
	case contract.ApplicationNavigationErrorNotFound:
		return &dialect.Response{Status: http.StatusNotFound, Body: wire.SFErrorResponse(wire.SFCodeNotFound, message)}
	case contract.ApplicationNavigationErrorConflict:
		return &dialect.Response{Status: http.StatusConflict, Body: wire.SFErrorResponse(wire.SFCodeConflict, message)}
	case contract.ApplicationNavigationErrorForbidden:
		return &dialect.Response{Status: http.StatusForbidden, Body: wire.SFErrorResponse(wire.SFCodeForbidden, message)}
	case contract.ApplicationNavigationErrorInternal:
		logApplicationNavigationInternalError("application navigation contract returned internal error", err)
		return applicationNavigationInternalErrorResponse()
	default:
		logApplicationNavigationInternalError("application navigation contract returned server error", err)
		return applicationNavigationInternalErrorResponse()
	}
}

func applicationNavigationInternalErrorResponse() *dialect.Response {
	return internalErrorResponse(applicationNavigationGenericInternalErrorMessage)
}

func logApplicationNavigationInternalError(message string, err error) {
	if err != nil {
		if cause := errors.Unwrap(err); cause != nil {
			log.Printf("ui apps: %s: %v", message, cause)
			return
		}
		log.Printf("ui apps: %s: %v", message, err)
		return
	}
	log.Printf("ui apps: %s", message)
}

func toolingApplicationSuccess(status int, id string) *dialect.Response {
	return jsonResponse(status, map[string]any{"id": id, "success": true, "errors": []any{}})
}
