package metadata

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strings"

	"github.com/nexuscrm/metadata/programchecker"
	"github.com/nexuscrm/shared/pkg/models"
)

const flowProgramSourceStandard = "standard"

type InstalledPackFlowProgramCheckOptions struct {
	OwnerPacks            []string
	Formula               programchecker.FormulaCatalogReader
	Actions               programchecker.ActionSignatureReader
	Connectors            programchecker.ConnectorMappingReader
	NormalizeFlowMetadata func(models.FlowMetadata) models.FlowMetadata
}

// CheckInstalledPackFlowPrograms validates pack-owned active Flow metadata
// against the MetadataService effective schema snapshot. A pack-authored Draft
// is deliberately non-serving and is therefore not an active-program target.
// Admin-customized FlowDefinitions are skipped because their active versions are
// L2 payload, not pack-owned L1 program metadata.
func (ms *MetadataService) CheckInstalledPackFlowPrograms(ctx context.Context, opts InstalledPackFlowProgramCheckOptions) (programchecker.Diagnostics, error) {
	if ms == nil {
		return nil, fmt.Errorf("metadata service is required")
	}
	metadataReader := NewProgramMetadataReader(ms)
	formulas := opts.Formula
	if formulas == nil {
		reader, err := programchecker.NewActiveFormulaCatalogReader()
		if err != nil {
			return nil, fmt.Errorf("build active formula catalog reader: %w", err)
		}
		formulas = reader
	}

	targets := installedPackFlowProgramTargets(ms.GetFlowDefinitions(ctx), ownerPackFilter(opts.OwnerPacks))
	if len(targets) == 0 {
		return nil, nil
	}
	var out programchecker.Diagnostics

	versionIDs := make([]string, 0, len(targets))
	for _, target := range targets {
		versionID := target.activeVersionID
		if versionID == "" {
			versionID = target.latestVersionID
		}
		if versionID != "" {
			versionIDs = append(versionIDs, versionID)
		}
	}

	versions := map[string]*models.FlowVersion{}
	if len(versionIDs) > 0 {
		if ms.repo == nil {
			return nil, fmt.Errorf("metadata repository is required to load Flow versions")
		}
		loaded, err := ms.GetFlowVersionsByIDs(ctx, versionIDs)
		if err != nil {
			return nil, fmt.Errorf("load active Flow versions for metadata program check: %w", err)
		}
		versions = loaded
	}

	for _, target := range targets {
		def := target.definition
		ownerPack := strings.TrimSpace(def.OwnerPack)
		componentPath := installedPackFlowProgramComponentPath(def)
		if target.activeVersionID == "" {
			if installedPackFlowProgramIsIntentionalDraft(def, versions[target.latestVersionID]) {
				continue
			}
			out = append(out, installedPackFlowProgramDiagnostic(def, componentPath, "metadata_program.missing_active_flow", "pack-owned FlowDefinition has no active Flow version"))
			continue
		}
		version := versions[target.activeVersionID]
		if version == nil {
			out = append(out, installedPackFlowProgramDiagnostic(def, componentPath, "metadata_program.missing_active_flow_version", fmt.Sprintf("active Flow version %q is missing", target.activeVersionID)))
			continue
		}
		programVersion, diag, err := ms.installedPackFlowProgramVersion(ctx, def, version, componentPath)
		if err != nil {
			return nil, err
		}
		if diag != nil {
			out = append(out, *diag)
			continue
		}
		metadata, metadataDiag := installedPackFlowProgramMetadata(def, programVersion, componentPath)
		if metadataDiag != nil {
			out = append(out, *metadataDiag)
			continue
		}
		if opts.NormalizeFlowMetadata != nil {
			metadata = opts.NormalizeFlowMetadata(metadata)
		}
		diags := programchecker.CheckFlowMetadata(ctx, metadata, programchecker.FlowCheckOptions{
			Metadata:      metadataReader,
			Formula:       formulas,
			Actions:       opts.Actions,
			Connectors:    opts.Connectors,
			ProcessType:   installedPackFlowProgramProcessType(def, programVersion),
			ComponentPath: componentPath,
			OwnerPack:     ownerPack,
		})
		out = append(out, diags...)
	}
	return out, nil
}

func installedPackFlowProgramIsIntentionalDraft(def *models.FlowDefinition, latestVersion *models.FlowVersion) bool {
	return def != nil &&
		latestVersion != nil &&
		strings.TrimSpace(latestVersion.DefinitionId) == strings.TrimSpace(def.Id) &&
		!latestVersion.IsDeleted &&
		isStandardFlowProgramVersion(latestVersion) &&
		strings.EqualFold(strings.TrimSpace(latestVersion.Status), models.FlowStatusDraft)
}

func installedPackFlowProgramProcessType(def *models.FlowDefinition, version *models.FlowVersion) string {
	if version != nil && strings.TrimSpace(version.ProcessType) != "" {
		return strings.TrimSpace(version.ProcessType)
	}
	if def != nil {
		return strings.TrimSpace(def.ProcessType)
	}
	return ""
}

func installedPackFlowProgramMetadata(def *models.FlowDefinition, version *models.FlowVersion, componentPath string) (models.FlowMetadata, *programchecker.Diagnostic) {
	if version == nil {
		return models.FlowMetadata{}, nil
	}
	raw := strings.TrimSpace(version.MetadataJSON)
	if raw == "" {
		return version.Metadata, nil
	}
	var metadata models.FlowMetadata
	if err := json.Unmarshal([]byte(raw), &metadata); err != nil {
		diag := installedPackFlowProgramDiagnostic(def, componentPath, "metadata_program.invalid_flow_metadata_json", fmt.Sprintf("Flow version %q metadata is not valid JSON: %v", version.Id, err))
		return models.FlowMetadata{}, &diag
	}
	return metadata, nil
}

func (ms *MetadataService) installedPackFlowProgramVersion(ctx context.Context, def *models.FlowDefinition, activeVersion *models.FlowVersion, componentPath string) (*models.FlowVersion, *programchecker.Diagnostic, error) {
	if def == nil || activeVersion == nil {
		return nil, nil, nil
	}
	if strings.TrimSpace(activeVersion.DefinitionId) != strings.TrimSpace(def.Id) {
		diag := installedPackFlowProgramDiagnostic(def, componentPath, "metadata_program.invalid_active_flow_version_definition", fmt.Sprintf("active Flow version %q belongs to FlowDefinition %q, want %q", activeVersion.Id, activeVersion.DefinitionId, def.Id))
		return nil, &diag, nil
	}
	if activeVersion.IsDeleted {
		diag := installedPackFlowProgramDiagnostic(def, componentPath, "metadata_program.deleted_active_flow_version", fmt.Sprintf("active Flow version %q is deleted", activeVersion.Id))
		return nil, &diag, nil
	}
	if !strings.EqualFold(strings.TrimSpace(activeVersion.Status), models.FlowStatusActive) {
		diag := installedPackFlowProgramDiagnostic(def, componentPath, "metadata_program.invalid_active_flow_version_status", fmt.Sprintf("active Flow version %q status = %q, want %q", activeVersion.Id, activeVersion.Status, models.FlowStatusActive))
		return nil, &diag, nil
	}
	if isStandardFlowProgramVersion(activeVersion) {
		return activeVersion, nil, nil
	}
	versions, err := ms.GetFlowVersions(ctx, def.Id)
	if err != nil {
		return nil, nil, fmt.Errorf("load Flow versions for standard metadata program %q: %w", def.DeveloperName, err)
	}
	standardVersion := latestLiveStandardFlowProgramVersion(versions)
	if standardVersion == nil {
		diag := installedPackFlowProgramDiagnostic(def, componentPath, "metadata_program.missing_standard_flow_version", fmt.Sprintf("active Flow version %q Source = %q, but no live Source=%q Flow version exists for the pack-owned definition", activeVersion.Id, activeVersion.Source, flowProgramSourceStandard))
		return nil, &diag, nil
	}
	return standardVersion, nil, nil
}

type installedPackFlowProgramTarget struct {
	definition      *models.FlowDefinition
	activeVersionID string
	latestVersionID string
}

func installedPackFlowProgramTargets(definitions []*models.FlowDefinition, filter map[string]struct{}) []installedPackFlowProgramTarget {
	targets := make([]installedPackFlowProgramTarget, 0, len(definitions))
	for _, def := range definitions {
		if def == nil || def.IsDeleted || def.IsCustomized {
			continue
		}
		ownerPack := strings.TrimSpace(def.OwnerPack)
		if ownerPack == "" {
			continue
		}
		if len(filter) > 0 {
			if _, ok := filter[strings.ToLower(ownerPack)]; !ok {
				continue
			}
		}
		targets = append(targets, installedPackFlowProgramTarget{
			definition:      def,
			activeVersionID: strings.TrimSpace(def.ActiveVersionId),
			latestVersionID: strings.TrimSpace(def.LatestVersionId),
		})
	}
	sort.SliceStable(targets, func(i, j int) bool {
		left := strings.ToLower(strings.TrimSpace(targets[i].definition.OwnerPack)) + "/" + strings.ToLower(strings.TrimSpace(targets[i].definition.DeveloperName))
		right := strings.ToLower(strings.TrimSpace(targets[j].definition.OwnerPack)) + "/" + strings.ToLower(strings.TrimSpace(targets[j].definition.DeveloperName))
		return left < right
	})
	return targets
}

func ownerPackFilter(ownerPacks []string) map[string]struct{} {
	out := map[string]struct{}{}
	for _, ownerPack := range ownerPacks {
		ownerPack = strings.ToLower(strings.TrimSpace(ownerPack))
		if ownerPack != "" {
			out[ownerPack] = struct{}{}
		}
	}
	return out
}

func latestLiveStandardFlowProgramVersion(versions []*models.FlowVersion) *models.FlowVersion {
	var out *models.FlowVersion
	for _, version := range versions {
		if !isStandardFlowProgramVersion(version) {
			continue
		}
		if out == nil || version.VersionNumber > out.VersionNumber ||
			(version.VersionNumber == out.VersionNumber && strings.TrimSpace(version.Id) > strings.TrimSpace(out.Id)) {
			out = version
		}
	}
	return out
}

func isStandardFlowProgramVersion(version *models.FlowVersion) bool {
	return version != nil &&
		!version.IsDeleted &&
		strings.EqualFold(strings.TrimSpace(version.Source), flowProgramSourceStandard)
}

func installedPackFlowProgramComponentPath(def *models.FlowDefinition) string {
	name := "unknown"
	if def != nil {
		if developerName := strings.TrimSpace(def.DeveloperName); developerName != "" {
			name = developerName
		} else if id := strings.TrimSpace(def.Id); id != "" {
			name = id
		}
	}
	return "standard_flow/" + name + ".flow.json"
}

func installedPackFlowProgramDiagnostic(def *models.FlowDefinition, componentPath, code, message string) programchecker.Diagnostic {
	diag := programchecker.Diagnostic{
		Code:          code,
		Severity:      programchecker.SeverityError,
		ComponentPath: componentPath,
		Message:       message,
	}
	if def != nil {
		diag.ElementName = strings.TrimSpace(def.DeveloperName)
		diag.OwnerPack = strings.TrimSpace(def.OwnerPack)
		diag.Reference = strings.TrimSpace(def.ActiveVersionId)
	}
	return diag
}
