package tenantoverlay

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

	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-controlplane/layoutownership"
	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
)

type relatedListOverlayData struct {
	Rules        []metadata.RelatedListRule `json:"rules"`
	Replacements []relatedListReplacement   `json:"replacements"`
}

type relatedListReplacement struct {
	LayoutObjectAPIName string `json:"layoutObjectApiName"`
	MatchObjectAPIName  string `json:"matchObjectApiName"`
	MatchLookupField    string `json:"matchLookupField"`
	TargetRuleKey       string `json:"targetRuleKey"`
}

// applyRelatedListOverlays rewrites tenant-specific related-list presentation
// metadata after the tenant object metadata exists. The platform layout can keep
// Salesforce-standard related list identities; opt-in overlay metadata decides
// tenant replacements.
func (p *Pack) applyRelatedListOverlays(ctx context.Context, db *sql.DB, specs []packs.ObjectSpec) error {
	overlay, ok, err := p.readRelatedListOverlayData()
	if err != nil {
		return err
	}
	if !ok {
		return nil
	}
	if len(overlay.Replacements) == 0 {
		return nil
	}

	listFieldsByObject := tenantListFieldsByObject(specs)
	ruleByKey := make(map[string]metadata.RelatedListRule, len(overlay.Rules))
	for _, rule := range overlay.Rules {
		ruleByKey[strings.ToLower(rule.Key)] = rule
	}
	for _, replacement := range overlay.Replacements {
		targetRule := ruleByKey[strings.ToLower(replacement.TargetRuleKey)]
		if err := applyRelatedListReplacementToLayouts(ctx, db, replacement, targetRule, listFieldsByObject); err != nil {
			return err
		}
	}
	return nil
}

func (p *Pack) RegisterRelatedListRules() error {
	overlay, ok, err := p.readRelatedListOverlayData()
	if err != nil {
		return err
	}
	if !ok {
		return metadata.RegisterTenantRelatedListRules(nil)
	}
	return metadata.RegisterTenantRelatedListRules(overlay.Rules)
}

func (p *Pack) readRelatedListOverlayData() (relatedListOverlayData, bool, error) {
	raw, ok, err := p.readOptional(RelatedListOverlaysFile)
	if err != nil {
		return relatedListOverlayData{}, false, err
	}
	if !ok {
		return relatedListOverlayData{}, false, nil
	}
	var overlay relatedListOverlayData
	if err := json.Unmarshal(raw, &overlay); err != nil {
		return relatedListOverlayData{}, false, fmt.Errorf("parse %s: %w", RelatedListOverlaysFile, err)
	}
	if err := overlay.validate(); err != nil {
		return relatedListOverlayData{}, false, err
	}
	return overlay, true, nil
}

func (overlay relatedListOverlayData) validate() error {
	if err := metadata.ValidateTenantRelatedListRules(overlay.Rules); err != nil {
		return fmt.Errorf("validate %s rules: %w", RelatedListOverlaysFile, err)
	}

	ruleByKey := make(map[string]metadata.RelatedListRule, len(overlay.Rules))
	for _, rule := range overlay.Rules {
		if rule.Key == "" {
			return fmt.Errorf("%s rule for %s.%s missing key", RelatedListOverlaysFile, rule.ChildObjectAPI, rule.LookupField)
		}
		ruleByKey[strings.ToLower(rule.Key)] = rule
	}
	for _, replacement := range overlay.Replacements {
		if err := replacement.validate(ruleByKey); err != nil {
			return err
		}
	}
	return nil
}

func tenantListFieldsByObject(specs []packs.ObjectSpec) map[string][]string {
	out := make(map[string][]string, len(specs))
	for _, spec := range specs {
		if len(spec.ListFields) == 0 {
			continue
		}
		out[strings.ToLower(spec.APIName)] = append([]string(nil), spec.ListFields...)
	}
	return out
}

func applyRelatedListReplacementToLayouts(
	ctx context.Context,
	db *sql.DB,
	replacement relatedListReplacement,
	targetRule metadata.RelatedListRule,
	listFieldsByObject map[string][]string,
) error {
	if err := replacement.validate(map[string]metadata.RelatedListRule{
		strings.ToLower(targetRule.Key): targetRule,
	}); err != nil {
		return err
	}

	query := fmt.Sprintf(
		"SELECT `%s`, `%s`, COALESCE(`%s`, 0), COALESCE(`%s`, '') "+
			"FROM `%s` WHERE `%s` = ? AND (`%s` = 0 OR `%s` IS NULL)",
		constants.ColID,
		constants.FieldSysPageLayout_Config,
		constants.FieldSysPageLayout_IsCustomized,
		constants.FieldSysPageLayout_Source,
		constants.TablePageLayout,
		constants.FieldSysPageLayout_ObjectAPIName,
		constants.FieldIsDeleted,
		constants.FieldIsDeleted,
	)
	rows, err := db.QueryContext(ctx, query, replacement.LayoutObjectAPIName)
	if err != nil {
		return fmt.Errorf("query %s related-list layouts: %w", replacement.LayoutObjectAPIName, err)
	}
	defer rows.Close()

	for rows.Next() {
		var (
			layoutID     string
			configJSON   sql.NullString
			isCustomized bool
			source       string
		)
		if err := rows.Scan(&layoutID, &configJSON, &isCustomized, &source); err != nil {
			return fmt.Errorf("scan %s related-list layout: %w", replacement.LayoutObjectAPIName, err)
		}
		if !configJSON.Valid || configJSON.String == "" {
			continue
		}

		layout, err := layoutownership.DecodePageLayoutConfig([]byte(configJSON.String))
		if err != nil {
			return fmt.Errorf("parse layout %s config: %w", layoutID, err)
		}

		allowModify := !layoutownership.IsAdminOwnedPageLayout(isCustomized, source)
		if !applyRelatedListReplacement(&layout, replacement, targetRule, listFieldsByObject, allowModify) {
			continue
		}

		nextJSON, err := json.Marshal(layout)
		if err != nil {
			return fmt.Errorf("marshal layout %s config: %w", layoutID, err)
		}
		if bytes.Equal(nextJSON, []byte(configJSON.String)) {
			continue
		}

		update := fmt.Sprintf(
			"UPDATE `%s` SET `%s` = ?, `%s` = ? WHERE `%s` = ?",
			constants.TablePageLayout,
			constants.FieldSysPageLayout_Config,
			constants.FieldSysPageLayout_LastModifiedDate,
			constants.ColID,
		)
		now := time.Now().Format("2006-01-02 15:04:05")
		if _, err := db.ExecContext(ctx, update, string(nextJSON), now, layoutID); err != nil {
			return fmt.Errorf("update layout %s related-list overlay: %w", layoutID, err)
		}
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterate %s related-list layouts: %w", replacement.LayoutObjectAPIName, err)
	}
	return nil
}

func (r relatedListReplacement) validate(ruleByKey map[string]metadata.RelatedListRule) error {
	targetRule := ruleByKey[strings.ToLower(r.TargetRuleKey)]
	switch {
	case r.LayoutObjectAPIName == "":
		return fmt.Errorf("related-list overlay missing layoutObjectApiName")
	case r.MatchObjectAPIName == "":
		return fmt.Errorf("related-list overlay for %s missing matchObjectApiName", r.LayoutObjectAPIName)
	case r.MatchLookupField == "":
		return fmt.Errorf("related-list overlay for %s missing matchLookupField", r.LayoutObjectAPIName)
	case r.TargetRuleKey == "":
		return fmt.Errorf("related-list overlay for %s missing targetRuleKey", r.LayoutObjectAPIName)
	case targetRule.Key == "":
		return fmt.Errorf("related-list overlay for %s references unknown targetRuleKey %s", r.LayoutObjectAPIName, r.TargetRuleKey)
	case !strings.EqualFold(targetRule.ParentObjectAPI, r.LayoutObjectAPIName):
		return fmt.Errorf("related-list overlay for %s points at rule %s owned by %s", r.LayoutObjectAPIName, r.TargetRuleKey, targetRule.ParentObjectAPI)
	default:
		return nil
	}
}

func applyRelatedListReplacement(
	layout *models.PageLayout,
	replacement relatedListReplacement,
	targetRule metadata.RelatedListRule,
	listFieldsByObject map[string][]string,
	allowModify bool,
) bool {
	if layout == nil || !allowModify {
		return false
	}

	targetIndex := -1
	matchIndex := -1
	for i, rl := range layout.RelatedLists {
		if relatedListMatches(rl, targetRule.ChildObjectAPI, targetRule.LookupField) {
			targetIndex = i
		}
		if relatedListMatches(rl, replacement.MatchObjectAPIName, replacement.MatchLookupField) {
			matchIndex = i
		}
	}

	target := replacement.targetRelatedList(targetRule, listFieldsByObject)
	switch {
	case targetIndex >= 0:
		changed := mergeRelatedList(&layout.RelatedLists[targetIndex], target)
		if matchIndex >= 0 && matchIndex != targetIndex {
			layout.RelatedLists = removeRelatedListAt(layout.RelatedLists, matchIndex)
			changed = true
		}
		return changed
	case matchIndex >= 0:
		layout.RelatedLists[matchIndex] = target
		return true
	case targetRule.AppendIfMissing:
		layout.RelatedLists = append(layout.RelatedLists, target)
		return true
	default:
		return false
	}
}

func (r relatedListReplacement) targetRelatedList(targetRule metadata.RelatedListRule, listFieldsByObject map[string][]string) models.RelatedListConfig {
	id := fmt.Sprintf("%s-%s", targetRule.ChildObjectAPI, targetRule.LookupField)
	fields := append([]string(nil), listFieldsByObject[strings.ToLower(targetRule.ChildObjectAPI)]...)
	if len(fields) == 0 && len(targetRule.Fields) > 0 {
		fields = append([]string(nil), targetRule.Fields...)
	}
	return models.RelatedListConfig{
		ID:             id,
		Label:          targetRule.Label,
		SecondaryLabel: targetRule.SecondaryLabel,
		ObjectAPIName:  targetRule.ChildObjectAPI,
		LookupField:    targetRule.LookupField,
		Fields:         fields,
	}
}

func relatedListMatches(rl models.RelatedListConfig, objectAPIName, lookupField string) bool {
	return strings.EqualFold(rl.ObjectAPIName, objectAPIName) &&
		strings.EqualFold(rl.LookupField, lookupField)
}

func mergeRelatedList(dst *models.RelatedListConfig, src models.RelatedListConfig) bool {
	changed := false
	if dst.ID != src.ID {
		dst.ID = src.ID
		changed = true
	}
	if dst.Label == "" && src.Label != "" {
		dst.Label = src.Label
		changed = true
	}
	if dst.SecondaryLabel == "" && src.SecondaryLabel != "" {
		dst.SecondaryLabel = src.SecondaryLabel
		changed = true
	}
	if !strings.EqualFold(dst.ObjectAPIName, src.ObjectAPIName) {
		dst.ObjectAPIName = src.ObjectAPIName
		changed = true
	}
	if !strings.EqualFold(dst.LookupField, src.LookupField) {
		dst.LookupField = src.LookupField
		changed = true
	}
	if len(dst.Fields) == 0 && len(src.Fields) > 0 {
		dst.Fields = append([]string(nil), src.Fields...)
		changed = true
	}
	return changed
}

func removeRelatedListAt(items []models.RelatedListConfig, idx int) []models.RelatedListConfig {
	if idx < 0 || idx >= len(items) {
		return items
	}
	out := make([]models.RelatedListConfig, 0, len(items)-1)
	out = append(out, items[:idx]...)
	out = append(out, items[idx+1:]...)
	return out
}
