package tenantoverlay

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/nexuscrm/backend/internal/bootstrap"
	"github.com/nexuscrm/backend/internal/controlplane/layoutownership"
	"github.com/nexuscrm/backend/internal/controlplane/packs"
	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

const standardPageLayoutOverlayBaselineSource = "standard_bootstrap"

type pageLayoutOverlaysFileData struct {
	Layouts []pageLayoutOverlay `json:"layouts"`
}

type pageLayoutOverlay struct {
	ObjectAPIName        string                `json:"objectApiName"`
	LayoutID             string                `json:"layoutId"`
	TabsReplace          []string              `json:"tabsReplace,omitempty"`
	CompactLayoutReplace []string              `json:"compactLayoutReplace,omitempty"`
	SectionsReplace      []models.PageSection  `json:"sectionsReplace,omitempty"`
	HeaderActionsAppend  []models.ActionConfig `json:"headerActionsAppend,omitempty"`
}

func (overlay pageLayoutOverlay) hasChanges() bool {
	return len(overlay.TabsReplace) > 0 ||
		len(overlay.CompactLayoutReplace) > 0 ||
		len(overlay.SectionsReplace) > 0 ||
		len(overlay.HeaderActionsAppend) > 0
}

func (overlay pageLayoutOverlay) validate() error {
	switch {
	case strings.TrimSpace(overlay.ObjectAPIName) == "":
		return fmt.Errorf("%s entry missing objectApiName", PageLayoutOverlaysFile)
	case !overlay.hasChanges():
		return fmt.Errorf("%s entry for %s (%s) has no overlay operations", PageLayoutOverlaysFile, overlay.ObjectAPIName, overlay.LayoutID)
	default:
		return nil
	}
}

func (data pageLayoutOverlaysFileData) validate() error {
	if len(data.Layouts) == 0 {
		return fmt.Errorf("%s must contain at least one layout overlay", PageLayoutOverlaysFile)
	}
	seenLayoutIDs := make(map[string]struct{}, len(data.Layouts))
	for _, overlay := range data.Layouts {
		if err := overlay.validate(); err != nil {
			return err
		}
		key := strings.ToLower(strings.TrimSpace(overlay.LayoutID))
		if key == "" {
			key = "object:" + strings.ToLower(strings.TrimSpace(overlay.ObjectAPIName))
		}
		if _, ok := seenLayoutIDs[key]; ok {
			return fmt.Errorf("%s contains duplicate overlay target %q", PageLayoutOverlaysFile, key)
		}
		seenLayoutIDs[key] = struct{}{}
	}
	return nil
}

func (p *Pack) applyPageLayoutOverlays(ctx context.Context, db *sql.DB) error {
	overlays, ok, err := p.readPageLayoutOverlays()
	if err != nil {
		return err
	}
	if !ok {
		return nil
	}

	for _, overlay := range overlays.Layouts {
		if err := applySinglePageLayoutOverlay(ctx, db, overlay); err != nil {
			return err
		}
	}
	return nil
}

func (p *Pack) readPageLayoutOverlays() (pageLayoutOverlaysFileData, bool, error) {
	raw, ok, err := p.readOptional(PageLayoutOverlaysFile)
	if err != nil {
		return pageLayoutOverlaysFileData{}, false, err
	}
	if !ok {
		return pageLayoutOverlaysFileData{}, false, nil
	}

	overlayData, err := decodePageLayoutOverlays(raw)
	if err != nil {
		return pageLayoutOverlaysFileData{}, false, apperrors.NewInternalError(
			fmt.Sprintf("failed to parse %s", PageLayoutOverlaysFile),
			err,
		)
	}
	if err := overlayData.validate(); err != nil {
		return pageLayoutOverlaysFileData{}, false, err
	}
	return overlayData, true, nil
}

func applySinglePageLayoutOverlay(ctx context.Context, db *sql.DB, overlay pageLayoutOverlay) error {
	currentRow, resolvedLayoutID, err := resolvePageLayoutOverlayTarget(ctx, db, overlay)
	if err != nil {
		return err
	}
	if currentRow == nil {
		return fmt.Errorf("%s overlay target resolved to empty layout row", overlay.ObjectAPIName)
	}
	if layoutownership.IsAdminOwnedPageLayout(currentRow.IsCustomized, currentRow.Source) {
		log.Printf("   ⚠️  %s layout %q is admin-owned; skipping tenant overlay reconcile", overlay.ObjectAPIName, resolvedLayoutID)
		return nil
	}
	if currentRow.ConfigJSON == "" {
		return fmt.Errorf("%s layout %q has empty Config; apply state is inconsistent", overlay.ObjectAPIName, resolvedLayoutID)
	}

	var layout models.PageLayout
	if err := json.Unmarshal([]byte(currentRow.ConfigJSON), &layout); err != nil {
		return fmt.Errorf("parse current %s layout %q config: %w", overlay.ObjectAPIName, resolvedLayoutID, err)
	}
	if !strings.EqualFold(strings.TrimSpace(layout.ObjectAPIName), strings.TrimSpace(overlay.ObjectAPIName)) {
		return fmt.Errorf("%s entry for layout %q targets config object %q", PageLayoutOverlaysFile, resolvedLayoutID, layout.ObjectAPIName)
	}
	if err := mergePageLayoutOverlay(&layout, overlay); err != nil {
		return err
	}
	layout.Source = layoutownership.PageLayoutSourceTenantOverlay

	newConfig, err := json.Marshal(layout)
	if err != nil {
		return fmt.Errorf("marshal merged %s layout %q: %w", overlay.ObjectAPIName, resolvedLayoutID, err)
	}
	if bytes.Equal(newConfig, []byte(currentRow.ConfigJSON)) {
		return nil
	}

	updateSQL := fmt.Sprintf(
		"UPDATE `%s` SET `%s` = ?, `%s` = ?, `%s` = 0, `%s` = ? WHERE `%s` = ?",
		constants.TablePageLayout,
		constants.FieldSysPageLayout_Config,
		constants.FieldSysPageLayout_Source,
		constants.FieldSysPageLayout_IsCustomized,
		constants.FieldSysPageLayout_LastModifiedDate,
		constants.ColID,
	)
	now := time.Now().Format("2006-01-02 15:04:05")
	if _, err := db.ExecContext(ctx, updateSQL, string(newConfig), layoutownership.PageLayoutSourceTenantOverlay, now, resolvedLayoutID); err != nil {
		return fmt.Errorf("update %s layout %q config: %w", overlay.ObjectAPIName, resolvedLayoutID, err)
	}
	return nil
}

func resolvePageLayoutOverlayTarget(ctx context.Context, db *sql.DB, overlay pageLayoutOverlay) (*layoutownership.PageLayoutRow, string, error) {
	if layoutID := strings.TrimSpace(overlay.LayoutID); layoutID != "" {
		currentRow, err := layoutownership.LoadPageLayoutRow(ctx, db, layoutID)
		if err != nil {
			return nil, "", fmt.Errorf("fetch %s layout %q: %w", overlay.ObjectAPIName, layoutID, err)
		}
		if currentRow == nil {
			return nil, "", fmt.Errorf("%s layout %q missing; apply-tenant requires the managed baseline first", overlay.ObjectAPIName, layoutID)
		}
		return currentRow, layoutID, nil
	}

	wildcardTarget, err := layoutownership.LoadWildcardLayoutTarget(ctx, db, overlay.ObjectAPIName)
	if err != nil {
		return nil, "", fmt.Errorf("fetch %s wildcard layout target: %w", overlay.ObjectAPIName, err)
	}
	if wildcardTarget != nil {
		currentRow, err := layoutownership.LoadPageLayoutRow(ctx, db, wildcardTarget.LayoutID)
		if err != nil {
			return nil, "", fmt.Errorf("fetch %s wildcard layout %q: %w", overlay.ObjectAPIName, wildcardTarget.LayoutID, err)
		}
		if currentRow == nil {
			return nil, "", fmt.Errorf("%s wildcard layout %q missing; apply-tenant requires the managed baseline first", overlay.ObjectAPIName, wildcardTarget.LayoutID)
		}
		return currentRow, wildcardTarget.LayoutID, nil
	}

	var currentRow *layoutownership.PageLayoutRow
	var layoutID string
	var hasStandardBaseline bool
	currentRow, layoutID, hasStandardBaseline, err = ensureStandardPageLayoutOverlayBaseline(ctx, db, overlay.ObjectAPIName)
	if err != nil {
		return nil, "", err
	}
	if hasStandardBaseline {
		return currentRow, layoutID, nil
	}

	liveCount, err := layoutownership.CountLivePageLayoutsForObject(ctx, db, overlay.ObjectAPIName)
	if err != nil {
		return nil, "", fmt.Errorf("count live %s page layouts: %w", overlay.ObjectAPIName, err)
	}
	if liveCount == 0 {
		return nil, "", fmt.Errorf("%s has no live managed page layout row; apply-tenant requires the managed baseline first", overlay.ObjectAPIName)
	}
	if liveCount > 1 {
		return nil, "", fmt.Errorf("%s has %d live page layout rows and no wildcard default assignment; page_layout_overlays.json must specify layoutId", overlay.ObjectAPIName, liveCount)
	}

	currentRow, err = layoutownership.LoadSingleLivePageLayoutRowForObject(ctx, db, overlay.ObjectAPIName)
	if err != nil {
		return nil, "", fmt.Errorf("load single live %s page layout row: %w", overlay.ObjectAPIName, err)
	}
	if currentRow == nil {
		return nil, "", fmt.Errorf("%s has one live page layout count but no loadable row", overlay.ObjectAPIName)
	}
	if layoutownership.IsAdminOwnedPageLayout(currentRow.IsCustomized, currentRow.Source) {
		return nil, "", fmt.Errorf("%s has one live admin-owned page layout row and no wildcard default assignment; page_layout_overlays.json must specify layoutId or restore the managed baseline first", overlay.ObjectAPIName)
	}
	return currentRow, currentRow.ID, nil
}

func ensureStandardPageLayoutOverlayBaseline(ctx context.Context, db *sql.DB, objectAPIName string) (*layoutownership.PageLayoutRow, string, bool, error) {
	layout, err := bootstrap.LoadStandardPageLayoutByObject(objectAPIName)
	if err != nil {
		return nil, "", false, fmt.Errorf("load standard %s layout baseline: %w", objectAPIName, err)
	}
	if layout == nil {
		return nil, "", false, nil
	}

	currentRow, err := layoutownership.LoadPageLayoutRow(ctx, db, layout.ID)
	if err != nil {
		return nil, "", false, fmt.Errorf("query standard %s layout baseline %q: %w", objectAPIName, layout.ID, err)
	}
	if currentRow != nil {
		return currentRow, layout.ID, true, nil
	}

	if err := packs.HydrateLayoutRelatedListsFromMetadata(db, layout); err != nil {
		return nil, "", false, fmt.Errorf("hydrate standard %s layout related lists: %w", objectAPIName, err)
	}
	layout.Source = standardPageLayoutOverlayBaselineSource
	layout.GenerationVersion = 0
	layout.IsCustomized = false
	layout.LayoutVersion = 0
	if err := layoutownership.UpsertManagedPageLayout(ctx, db, *layout, layout.Source); err != nil {
		return nil, "", false, fmt.Errorf("materialize standard %s layout baseline %q: %w", objectAPIName, layout.ID, err)
	}

	currentRow, err = layoutownership.LoadPageLayoutRow(ctx, db, layout.ID)
	if err != nil {
		return nil, "", false, fmt.Errorf("reload standard %s layout baseline %q: %w", objectAPIName, layout.ID, err)
	}
	if currentRow == nil {
		return nil, "", false, fmt.Errorf("materialized standard %s layout baseline %q but could not reload it", objectAPIName, layout.ID)
	}
	return currentRow, layout.ID, true, nil
}

func mergePageLayoutOverlay(layout *models.PageLayout, overlay pageLayoutOverlay) error {
	if len(overlay.TabsReplace) > 0 {
		tabs, err := normalizeOverlayTabs(overlay.TabsReplace)
		if err != nil {
			return err
		}
		layout.Tabs = tabs
	}
	if len(overlay.CompactLayoutReplace) > 0 {
		layout.CompactLayout = append([]string(nil), overlay.CompactLayoutReplace...)
	}
	if len(overlay.SectionsReplace) > 0 {
		sections, err := cloneAndValidateOverlaySections(overlay.SectionsReplace, layout.Tabs)
		if err != nil {
			return err
		}
		layout.Sections = sections
	}

	if len(overlay.HeaderActionsAppend) > 0 {
		existing := make(map[string]struct{}, len(layout.HeaderActions))
		for _, action := range layout.HeaderActions {
			existing[action.Name] = struct{}{}
		}
		for _, action := range overlay.HeaderActionsAppend {
			if action.Name == "" {
				return fmt.Errorf("overlay headerActionsAppend entry missing name")
			}
			if _, ok := existing[action.Name]; ok {
				continue
			}
			layout.HeaderActions = append(layout.HeaderActions, action)
			existing[action.Name] = struct{}{}
		}
	}

	return nil
}

func normalizeOverlayTabs(tabs []string) ([]string, error) {
	normalized := make([]string, 0, len(tabs))
	seen := make(map[string]struct{}, len(tabs))
	for _, tab := range tabs {
		if tab == "" {
			return nil, fmt.Errorf("overlay tabsReplace contains empty tab")
		}
		if _, ok := seen[tab]; ok {
			return nil, fmt.Errorf("overlay tabsReplace contains duplicate tab %q", tab)
		}
		seen[tab] = struct{}{}
		normalized = append(normalized, tab)
	}
	return normalized, nil
}

func cloneAndValidateOverlaySections(sections []models.PageSection, tabs []string) ([]models.PageSection, error) {
	tabSet := make(map[string]struct{}, len(tabs))
	for _, tab := range tabs {
		tabSet[tab] = struct{}{}
	}

	cloned := make([]models.PageSection, 0, len(sections))
	seenIDs := make(map[string]struct{}, len(sections))
	for _, section := range sections {
		if section.ID == "" {
			return nil, fmt.Errorf("overlay sectionsReplace contains section with empty id")
		}
		if _, ok := seenIDs[section.ID]; ok {
			return nil, fmt.Errorf("overlay sectionsReplace contains duplicate section id %q", section.ID)
		}
		seenIDs[section.ID] = struct{}{}
		if section.Tab != nil {
			if _, ok := tabSet[*section.Tab]; !ok {
				return nil, fmt.Errorf("overlay section %q references unknown tab %q", section.ID, *section.Tab)
			}
		}
		cloned = append(cloned, cloneOverlaySection(section))
	}
	return cloned, nil
}

func cloneOverlaySection(section models.PageSection) models.PageSection {
	cloned := section
	cloned.Fields = append([]string(nil), section.Fields...)
	if section.FieldLabels != nil {
		cloned.FieldLabels = make(map[string]string, len(section.FieldLabels))
		for k, v := range section.FieldLabels {
			cloned.FieldLabels[k] = v
		}
	}
	if section.ComponentConfig != nil {
		cloned.ComponentConfig = make(map[string]interface{}, len(section.ComponentConfig))
		for k, v := range section.ComponentConfig {
			cloned.ComponentConfig[k] = v
		}
	}
	if section.Tab != nil {
		tab := *section.Tab
		cloned.Tab = &tab
	}
	if section.Type != nil {
		sectionType := *section.Type
		cloned.Type = &sectionType
	}
	if section.ComponentName != nil {
		componentName := *section.ComponentName
		cloned.ComponentName = &componentName
	}
	if section.VisibilityCondition != nil {
		visibility := *section.VisibilityCondition
		cloned.VisibilityCondition = &visibility
	}
	return cloned
}

func decodePageLayoutOverlays(data []byte) (pageLayoutOverlaysFileData, error) {
	var overlayData pageLayoutOverlaysFileData
	dec := json.NewDecoder(bytes.NewReader(data))
	dec.DisallowUnknownFields()
	if err := dec.Decode(&overlayData); err != nil {
		return pageLayoutOverlaysFileData{}, err
	}
	return overlayData, nil
}
