package tenantoverlay

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/nexuscrm/backend/internal/controlplane/packs"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/salesforce"
)

// flowFilenameSuffix bounds which files are treated as flows.
// Anything else in the flows/ directory (e.g. README, .keep) is ignored.
const flowFilenameSuffix = ".flow.json"

// retiredTenantFlowDeveloperNames enumerates tenant flow DeveloperNames that
// used to ship from this quarantine directory but no longer do. When a flow is
// removed from the pack, add its DeveloperName here in the same commit so apply
// can soft-delete the previously managed definition without reintroducing a
// package-ownership registry.
var retiredTenantFlowDeveloperNames = []string{}

// seedTenantFlows reconciles tenants/flows/*.flow.json into the
// FlowDefinition / Flow tables. FlowDefinition.IsCustomized is the ownership
// guard: tenant apply may reconcile only definitions that remain
// IsCustomized=0.
func (p *Pack) seedTenantFlows(ctx context.Context, deps packs.ApplyDeps) error {
	flowDir := p.path(FlowsDir)
	entries, err := os.ReadDir(flowDir)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil
		}
		return fmt.Errorf("read flows directory: %w", err)
	}

	shipped := make(map[string]struct{}, len(entries))
	names := make([]string, 0, len(entries))
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), flowFilenameSuffix) {
			continue
		}
		names = append(names, e.Name())
	}
	sort.Strings(names)

	for _, fileName := range names {
		developerName := strings.TrimSuffix(fileName, flowFilenameSuffix)
		shipped[developerName] = struct{}{}

		raw, err := os.ReadFile(filepath.Join(flowDir, fileName))
		if err != nil {
			return fmt.Errorf("read flow %q: %w", fileName, err)
		}
		if err := withTenantFlowWriteTx(ctx, deps, func(txCtx context.Context) error {
			return reconcileTenantFlow(txCtx, deps, developerName, raw)
		}); err != nil {
			return fmt.Errorf("reconcile flow %q: %w", developerName, err)
		}
	}

	return withTenantFlowWriteTx(ctx, deps, func(txCtx context.Context) error {
		return softDeleteMissingTenantFlows(txCtx, deps, shipped)
	})
}

func withTenantFlowWriteTx(ctx context.Context, deps packs.ApplyDeps, fn func(context.Context) error) error {
	if deps.TxManager == nil {
		return fn(ctx)
	}
	return deps.TxManager.WithTransaction(ctx, fn)
}

// reconcileTenantFlow converges one FlowDefinition and its versions onto the
// shipped JSON. Customized rows are preserved with an explicit warning rather
// than being silently overwritten.
func reconcileTenantFlow(ctx context.Context, deps packs.ApplyDeps, developerName string, rawJSON []byte) error {
	canonical, err := canonicalizeFlowMetadata(rawJSON)
	if err != nil {
		return fmt.Errorf("canonicalize metadata: %w", err)
	}

	var meta models.FlowMetadata
	if err := json.Unmarshal(rawJSON, &meta); err != nil {
		return fmt.Errorf("parse metadata: %w", err)
	}
	processType := extractFlowProcessType(rawJSON)

	existing, err := deps.MetadataRepo.GetFlowDefinitionByName(ctx, developerName)
	if err != nil {
		return fmt.Errorf("lookup existing definition: %w", err)
	}
	if existing != nil && existing.IsCustomized {
		log.Printf("   ⚠️  tenant flow %s: preserved customized FlowDefinition; skipping tenant reconcile", developerName)
		return nil
	}

	if existing == nil {
		existing = &models.FlowDefinition{
			Id:            newTenantFlowID("FlowDefinition"),
			DeveloperName: developerName,
		}
		if err := deps.MetadataRepo.CreateFlowDefinition(ctx, existing); err != nil {
			return fmt.Errorf("create definition: %w", err)
		}
	}

	targetVersionID, err := ensureActiveFlowVersionMatches(ctx, deps, existing, &meta, processType, canonical)
	if err != nil {
		return err
	}

	target := &models.FlowDefinition{
		DeveloperName:        existing.DeveloperName,
		MasterLabel:          firstNonEmpty(meta.Label, developerName),
		Description:          meta.Description,
		ProcessType:          processType,
		TriggerType:          startTriggerType(&meta),
		TriggerObjectOrEvent: startTriggerObject(&meta),
		ActiveVersionId:      targetVersionID,
		LatestVersionId:      targetVersionID,
		IsDeleted:            false,
	}
	if err := deps.MetadataRepo.UpdateFlowDefinition(ctx, existing.Id, target); err != nil {
		return fmt.Errorf("sync definition projection: %w", err)
	}
	return nil
}

// ensureActiveFlowVersionMatches returns the Id of a FlowVersion whose
// (canonical metadata, processType) tuple equals the shipped tuple
// AND whose Status is Active.
func ensureActiveFlowVersionMatches(
	ctx context.Context,
	deps packs.ApplyDeps,
	existing *models.FlowDefinition,
	meta *models.FlowMetadata,
	processType string,
	canonicalMetadata []byte,
) (string, error) {
	if existing.ActiveVersionId != "" {
		active, err := deps.MetadataRepo.GetFlowVersion(ctx, existing.ActiveVersionId)
		if err != nil {
			return "", fmt.Errorf("load active version: %w", err)
		}
		if active != nil {
			stored, err := canonicalizeFlowMetadata([]byte(active.MetadataJSON))
			if err != nil {
				return "", fmt.Errorf("canonicalize stored metadata: %w", err)
			}
			if bytesEqual(stored, canonicalMetadata) &&
				active.ProcessType == processType &&
				strings.EqualFold(active.Source, "custom") {
				return active.Id, nil
			}
		}
	}

	versions, err := deps.MetadataRepo.GetFlowVersions(ctx, existing.Id)
	if err != nil {
		return "", fmt.Errorf("list versions: %w", err)
	}
	nextVer := 1
	for _, v := range versions {
		if v.VersionNumber >= nextVer {
			nextVer = v.VersionNumber + 1
		}
	}

	newVersionID := newTenantFlowID("Flow")
	if err := deps.MetadataRepo.CreateTenantFlowVersion(ctx, &models.FlowVersion{
		Id:            newVersionID,
		DefinitionId:  existing.Id,
		VersionNumber: nextVer,
		Status:        models.FlowStatusDraft,
		Description:   meta.Description,
		ProcessType:   processType,
		MetadataRaw:   canonicalMetadata,
	}); err != nil {
		return "", fmt.Errorf("create version v%d: %w", nextVer, err)
	}
	if err := deps.MetadataRepo.ActivateFlowVersion(ctx, newVersionID); err != nil {
		return "", fmt.Errorf("activate version v%d: %w", nextVer, err)
	}
	return newVersionID, nil
}

func softDeleteMissingTenantFlows(ctx context.Context, deps packs.ApplyDeps, shipped map[string]struct{}) error {
	for _, developerName := range retiredTenantFlowDeveloperNames {
		if _, keep := shipped[developerName]; keep {
			continue
		}
		def, err := deps.MetadataRepo.GetFlowDefinitionByName(ctx, developerName)
		if err != nil {
			return fmt.Errorf("lookup retired flow %q: %w", developerName, err)
		}
		if def == nil || def.IsDeleted || def.IsCustomized {
			continue
		}
		target := &models.FlowDefinition{
			DeveloperName:        def.DeveloperName,
			MasterLabel:          def.MasterLabel,
			Description:          def.Description,
			ProcessType:          def.ProcessType,
			TriggerType:          def.TriggerType,
			TriggerObjectOrEvent: def.TriggerObjectOrEvent,
			ActiveVersionId:      "",
			LatestVersionId:      def.LatestVersionId,
			IsDeleted:            true,
		}
		if err := deps.MetadataRepo.UpdateFlowDefinition(ctx, def.Id, target); err != nil {
			return fmt.Errorf("soft-delete retired tenant flow %q: %w", developerName, err)
		}
	}
	return nil
}

// canonicalizeFlowMetadata parses the JSON into the typed FlowMetadata
// struct and re-marshals it.
func canonicalizeFlowMetadata(raw []byte) ([]byte, error) {
	if len(raw) == 0 {
		return []byte("{}"), nil
	}
	var meta models.FlowMetadata
	if err := json.Unmarshal(raw, &meta); err != nil {
		return nil, err
	}
	return json.Marshal(meta)
}

// extractFlowProcessType pulls "processType" out of the raw JSON because
// FlowMetadata does not model that field.
func extractFlowProcessType(raw []byte) string {
	var wire struct {
		ProcessType string `json:"processType"`
	}
	_ = json.Unmarshal(raw, &wire)
	if wire.ProcessType == "" {
		return "AutoLaunchedFlow"
	}
	return wire.ProcessType
}

func startTriggerType(meta *models.FlowMetadata) string {
	if meta == nil || meta.Start == nil {
		return ""
	}
	return meta.Start.TriggerType
}

func startTriggerObject(meta *models.FlowMetadata) string {
	if meta == nil || meta.Start == nil {
		return ""
	}
	return meta.Start.Object
}

func firstNonEmpty(candidates ...string) string {
	for _, c := range candidates {
		if c != "" {
			return c
		}
	}
	return ""
}

// newTenantFlowID generates a Salesforce-compatible 18-char record ID for
// a Flow SObject row. objectAPIName must be a standard SObject registered in
// shared/pkg/salesforce/compat.go (e.g. "FlowDefinition", "Flow").
func newTenantFlowID(objectAPIName string) string {
	prefix, ok := salesforce.StandardKeyPrefixForObject(objectAPIName)
	if !ok {
		panic(fmt.Sprintf("newTenantFlowID: no key prefix registered for %q", objectAPIName))
	}
	return fmt.Sprintf("%s%015d", prefix, time.Now().UnixNano()%1000000000000000)
}

func bytesEqual(a, b []byte) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
