// ABOUTME: Deployment planner for EdgeConnect apply command with intelligent state comparison
// ABOUTME: Analyzes desired vs current state to generate optimal deployment plans with minimal API calls
package v2

import (
	"context"
	"crypto/sha256"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
	v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)

// EdgeConnectClientInterface defines the methods needed for deployment planning
type EdgeConnectClientInterface interface {
	ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error)
	CreateApp(ctx context.Context, input *v2.NewAppInput) error
	UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error
	DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error
	ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) (v2.AppInstance, error)
	CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error
	UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error
	DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error
}

// Planner defines the interface for deployment planning
type Planner interface {
	// Plan analyzes the configuration and current state to generate a deployment plan
	Plan(ctx context.Context, config *config.EdgeConnectConfig) (*PlanResult, error)

	// PlanWithOptions allows customization of planning behavior
	PlanWithOptions(ctx context.Context, config *config.EdgeConnectConfig, opts PlanOptions) (*PlanResult, error)
}

// PlanOptions provides configuration for the planning process
type PlanOptions struct {
	// DryRun indicates this is a planning-only operation
	DryRun bool

	// Force indicates to proceed even with warnings
	Force bool

	// SkipStateCheck bypasses current state queries (useful for testing)
	SkipStateCheck bool

	// ParallelQueries enables parallel state fetching
	ParallelQueries bool

	// Timeout for API operations
	Timeout time.Duration
}

// DefaultPlanOptions returns sensible default planning options
func DefaultPlanOptions() PlanOptions {
	return PlanOptions{
		DryRun:          false,
		Force:           false,
		SkipStateCheck:  false,
		ParallelQueries: true,
		Timeout:         30 * time.Second,
	}
}

// EdgeConnectPlanner implements the Planner interface for EdgeConnect
type EdgeConnectPlanner struct {
	client EdgeConnectClientInterface
}

// NewPlanner creates a new EdgeConnect deployment planner
func NewPlanner(client EdgeConnectClientInterface) Planner {
	return &EdgeConnectPlanner{
		client: client,
	}
}

// Plan analyzes the configuration and generates a deployment plan
func (p *EdgeConnectPlanner) Plan(ctx context.Context, config *config.EdgeConnectConfig) (*PlanResult, error) {
	return p.PlanWithOptions(ctx, config, DefaultPlanOptions())
}

// PlanWithOptions generates a deployment plan with custom options
func (p *EdgeConnectPlanner) PlanWithOptions(ctx context.Context, config *config.EdgeConnectConfig, opts PlanOptions) (*PlanResult, error) {
	startTime := time.Now()
	var warnings []string

	// Create the deployment plan structure
	plan := &DeploymentPlan{
		ConfigName: config.Metadata.Name,
		CreatedAt:  startTime,
		DryRun:     opts.DryRun,
	}

	// Step 1: Plan application state
	appAction, appWarnings, err := p.planAppAction(ctx, config, opts)
	if err != nil {
		return &PlanResult{Error: err}, err
	}
	plan.AppAction = *appAction
	warnings = append(warnings, appWarnings...)

	// Step 2: Plan instance actions
	instanceActions, instanceWarnings, err := p.planInstanceActions(ctx, config, opts)
	if err != nil {
		return &PlanResult{Error: err}, err
	}
	plan.InstanceActions = instanceActions
	warnings = append(warnings, instanceWarnings...)

	// Step 3: Calculate plan metadata
	p.calculatePlanMetadata(plan)

	// Step 4: Generate summary
	plan.Summary = plan.GenerateSummary()

	// Step 5: Validate the plan
	if err := plan.Validate(); err != nil {
		return &PlanResult{Error: fmt.Errorf("invalid deployment plan: %w", err)}, err
	}

	return &PlanResult{
		Plan:     plan,
		Warnings: warnings,
	}, nil
}

// planAppAction determines what action needs to be taken for the application
func (p *EdgeConnectPlanner) planAppAction(ctx context.Context, config *config.EdgeConnectConfig, opts PlanOptions) (*AppAction, []string, error) {
	var warnings []string

	// Build desired app state
	desired := &AppState{
		Name:         config.Metadata.Name,
		Version:      config.Metadata.AppVersion,
		Organization: config.Metadata.Organization,        // Use first infra template for org
		Region:       config.Spec.InfraTemplate[0].Region, // Use first infra template for region
		Exists:       false,                               // Will be set based on current state
	}

	if config.Spec.IsK8sApp() {
		desired.AppType = AppTypeK8s
	} else {
		desired.AppType = AppTypeDocker
	}

	// Extract outbound connections from config
	if config.Spec.Network != nil {
		desired.OutboundConnections = make([]SecurityRule, len(config.Spec.Network.OutboundConnections))
		for i, conn := range config.Spec.Network.OutboundConnections {
			desired.OutboundConnections[i] = SecurityRule{
				Protocol:     conn.Protocol,
				PortRangeMin: conn.PortRangeMin,
				PortRangeMax: conn.PortRangeMax,
				RemoteCIDR:   conn.RemoteCIDR,
			}
		}
	}

	// Calculate manifest hash
	manifestHash, err := p.calculateManifestHash(config.Spec.GetManifestFile())
	if err != nil {
		return nil, warnings, fmt.Errorf("failed to calculate manifest hash: %w", err)
	}
	desired.ManifestHash = manifestHash

	action := &AppAction{
		Type:         ActionNone,
		Desired:      desired,
		ManifestHash: manifestHash,
		Reason:       "No action needed",
	}

	// Skip state check if requested (useful for testing)
	if opts.SkipStateCheck {
		action.Type = ActionCreate
		action.Reason = "Creating app (state check skipped)"
		action.Changes = []string{"Create new application"}
		return action, warnings, nil
	}

	// Query current app state
	current, err := p.getCurrentAppState(ctx, desired, opts.Timeout)
	if err != nil {
		// If app doesn't exist, we need to create it
		if isResourceNotFoundError(err) {
			action.Type = ActionCreate
			action.Reason = "Application does not exist"
			action.Changes = []string{"Create new application"}
			return action, warnings, nil
		}
		return nil, warnings, fmt.Errorf("failed to query current app state: %w", err)
	}

	action.Current = current

	// Compare current vs desired state
	changes, manifestChanged := p.compareAppStates(current, desired)
	action.ManifestChanged = manifestChanged

	if len(changes) > 0 {
		action.Type = ActionUpdate
		action.Changes = changes
		action.Reason = "Application configuration has changed"
		fmt.Printf("Changes: %v\n", changes)

		if manifestChanged {
			warnings = append(warnings, "Manifest file has changed - instances may need to be recreated")
		}
	}

	return action, warnings, nil
}

// planInstanceActions determines what actions need to be taken for instances
func (p *EdgeConnectPlanner) planInstanceActions(ctx context.Context, config *config.EdgeConnectConfig, opts PlanOptions) ([]InstanceAction, []string, error) {
	var actions []InstanceAction
	var warnings []string

	for _, infra := range config.Spec.InfraTemplate {
		instanceName := getInstanceName(config.Metadata.Name, config.Metadata.AppVersion)

		desired := &InstanceState{
			Name:         instanceName,
			AppVersion:   config.Metadata.AppVersion,
			Organization: config.Metadata.Organization,
			Region:       infra.Region,
			CloudletOrg:  infra.CloudletOrg,
			CloudletName: infra.CloudletName,
			FlavorName:   infra.FlavorName,
			Exists:       false,
		}

		action := &InstanceAction{
			Type:         ActionNone,
			Target:       infra,
			Desired:      desired,
			InstanceName: instanceName,
			Reason:       "No action needed",
		}

		// Skip state check if requested
		if opts.SkipStateCheck {
			action.Type = ActionCreate
			action.Reason = "Creating instance (state check skipped)"
			action.Changes = []string{"Create new instance"}
			actions = append(actions, *action)
			continue
		}

		// Query current instance state
		current, err := p.getCurrentInstanceState(ctx, desired, opts.Timeout)
		if err != nil {
			// If instance doesn't exist, we need to create it
			if isResourceNotFoundError(err) {
				action.Type = ActionCreate
				action.Reason = "Instance does not exist"
				action.Changes = []string{"Create new instance"}
				actions = append(actions, *action)
				continue
			}
			return nil, warnings, fmt.Errorf("failed to query current instance state: %w", err)
		}

		action.Current = current

		// Compare current vs desired state
		changes := p.compareInstanceStates(current, desired)
		if len(changes) > 0 {
			action.Type = ActionUpdate
			action.Changes = changes
			action.Reason = "Instance configuration has changed"
		}

		actions = append(actions, *action)
	}

	return actions, warnings, nil
}

// getCurrentAppState queries the current state of an application
func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *AppState, timeout time.Duration) (*AppState, error) {
	timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	appKey := v2.AppKey{
		Organization: desired.Organization,
		Name:         desired.Name,
		Version:      desired.Version,
	}

	app, err := p.client.ShowApp(timeoutCtx, appKey, desired.Region)
	if err != nil {
		return nil, err
	}

	current := &AppState{
		Name:         app.Key.Name,
		Version:      app.Key.Version,
		Organization: app.Key.Organization,
		Region:       desired.Region,
		Exists:       true,
		LastUpdated:  time.Now(), // EdgeConnect doesn't provide this, so use current time
	}

	// Calculate current manifest hash
	hasher := sha256.New()
	hasher.Write([]byte(app.DeploymentManifest))
	current.ManifestHash = fmt.Sprintf("%x", hasher.Sum(nil))

	// Note: EdgeConnect API doesn't currently support annotations for manifest hash tracking
	// This would be implemented when the API supports it

	// Determine app type based on deployment type
	if app.Deployment == "kubernetes" {
		current.AppType = AppTypeK8s
	} else {
		current.AppType = AppTypeDocker
	}

	// Extract outbound connections from the app
	current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
	for i, conn := range app.RequiredOutboundConnections {
		current.OutboundConnections[i] = SecurityRule(conn)
	}

	return current, nil
}

// getCurrentInstanceState queries the current state of an application instance
func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desired *InstanceState, timeout time.Duration) (*InstanceState, error) {
	timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	instanceKey := v2.AppInstanceKey{
		Organization: desired.Organization,
		Name:         desired.Name,
		CloudletKey: v2.CloudletKey{
			Organization: desired.CloudletOrg,
			Name:         desired.CloudletName,
		},
	}

	appKey := v2.AppKey{Name: desired.AppName}

	instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, appKey, desired.Region)
	if err != nil {
		return nil, err
	}

	current := &InstanceState{
		Name:         instance.Key.Name,
		AppName:      instance.AppKey.Name,
		AppVersion:   instance.AppKey.Version,
		Organization: instance.Key.Organization,
		Region:       desired.Region,
		CloudletOrg:  instance.Key.CloudletKey.Organization,
		CloudletName: instance.Key.CloudletKey.Name,
		FlavorName:   instance.Flavor.Name,
		State:        instance.State,
		Exists:       true,
		LastUpdated:  time.Now(), // EdgeConnect doesn't provide this
	}

	return current, nil
}

// compareAppStates compares current and desired app states and returns changes
func (p *EdgeConnectPlanner) compareAppStates(current, desired *AppState) ([]string, bool) {
	var changes []string
	manifestChanged := false

	// Compare manifest hash - only if both states have hash values
	// Since EdgeConnect API doesn't support annotations yet, skip manifest hash comparison for now
	// This would be implemented when the API supports manifest hash tracking
	if current.ManifestHash != "" && desired.ManifestHash != "" && current.ManifestHash != desired.ManifestHash {
		changes = append(changes, fmt.Sprintf("Manifest hash changed: %s -> %s", current.ManifestHash, desired.ManifestHash))
		manifestChanged = true
	}

	// Compare app type
	if current.AppType != desired.AppType {
		changes = append(changes, fmt.Sprintf("App type changed: %s -> %s", current.AppType, desired.AppType))
	}

	// Compare outbound connections
	outboundChanges := p.compareOutboundConnections(current.OutboundConnections, desired.OutboundConnections)
	if len(outboundChanges) > 0 {
		sb := strings.Builder{}
		sb.WriteString("Outbound connections changed:\n")
		for _, change := range outboundChanges {
			sb.WriteString(change)
			sb.WriteString("\n")
		}
		changes = append(changes, sb.String())
	}

	return changes, manifestChanged
}

// compareOutboundConnections compares two sets of outbound connections for equality
func (p *EdgeConnectPlanner) compareOutboundConnections(current, desired []SecurityRule) []string {
	var changes []string
	makeMap := func(rules []SecurityRule) map[string]SecurityRule {
		m := make(map[string]SecurityRule, len(rules))
		for _, r := range rules {
			key := fmt.Sprintf("%s:%d-%d:%s",
				strings.ToLower(r.Protocol),
				r.PortRangeMin,
				r.PortRangeMax,
				r.RemoteCIDR,
			)
			m[key] = r
		}
		return m
	}

	currentMap := makeMap(current)
	desiredMap := makeMap(desired)

	// Find added and modified rules
	for key, rule := range desiredMap {
		if _, exists := currentMap[key]; !exists {
			changes = append(changes, fmt.Sprintf("  - Added outbound connection: %s %d-%d to %s", rule.Protocol, rule.PortRangeMin, rule.PortRangeMax, rule.RemoteCIDR))
		}
	}

	// Find removed rules
	for key, rule := range currentMap {
		if _, exists := desiredMap[key]; !exists {
			changes = append(changes, fmt.Sprintf("  - Removed outbound connection: %s %d-%d to %s", rule.Protocol, rule.PortRangeMin, rule.PortRangeMax, rule.RemoteCIDR))
		}
	}

	return changes
}

// compareInstanceStates compares current and desired instance states and returns changes
func (p *EdgeConnectPlanner) compareInstanceStates(current, desired *InstanceState) []string {
	var changes []string

	if current.FlavorName != desired.FlavorName {
		changes = append(changes, fmt.Sprintf("Flavor changed: %s -> %s", current.FlavorName, desired.FlavorName))
	}

	if current.CloudletName != desired.CloudletName {
		changes = append(changes, fmt.Sprintf("Cloudlet changed: %s -> %s", current.CloudletName, desired.CloudletName))
	}

	if current.CloudletOrg != desired.CloudletOrg {
		changes = append(changes, fmt.Sprintf("Cloudlet org changed: %s -> %s", current.CloudletOrg, desired.CloudletOrg))
	}

	return changes
}

// calculateManifestHash computes the SHA256 hash of a manifest file
func (p *EdgeConnectPlanner) calculateManifestHash(manifestPath string) (string, error) {
	if manifestPath == "" {
		return "", nil
	}

	file, err := os.Open(manifestPath)
	if err != nil {
		return "", fmt.Errorf("failed to open manifest file: %w", err)
	}
	defer func() {
		_ = file.Close()
	}()

	hasher := sha256.New()
	if _, err := io.Copy(hasher, file); err != nil {
		return "", fmt.Errorf("failed to hash manifest file: %w", err)
	}

	return fmt.Sprintf("%x", hasher.Sum(nil)), nil
}

// calculatePlanMetadata computes metadata for the deployment plan
func (p *EdgeConnectPlanner) calculatePlanMetadata(plan *DeploymentPlan) {
	totalActions := 0

	if plan.AppAction.Type != ActionNone {
		totalActions++
	}

	for _, action := range plan.InstanceActions {
		if action.Type != ActionNone {
			totalActions++
		}
	}

	plan.TotalActions = totalActions

	// Estimate duration based on action types and counts
	plan.EstimatedDuration = p.estimateDeploymentDuration(plan)
}

// estimateDeploymentDuration provides a rough estimate of deployment time
func (p *EdgeConnectPlanner) estimateDeploymentDuration(plan *DeploymentPlan) time.Duration {
	var duration time.Duration

	// App operations
	switch plan.AppAction.Type {
	case ActionCreate:
		duration += 30 * time.Second
	case ActionUpdate:
		duration += 15 * time.Second
	}

	// Instance operations (can be done in parallel)
	instanceDuration := time.Duration(0)
	for _, action := range plan.InstanceActions {
		switch action.Type {
		case ActionCreate:
			instanceDuration = max(instanceDuration, 2*time.Minute)
		case ActionUpdate:
			instanceDuration = max(instanceDuration, 1*time.Minute)
		}
	}

	duration += instanceDuration

	// Add buffer time
	duration += 30 * time.Second

	return duration
}

// isResourceNotFoundError checks if an error indicates a resource was not found
func isResourceNotFoundError(err error) bool {
	if err == nil {
		return false
	}

	errStr := strings.ToLower(err.Error())
	return strings.Contains(errStr, "not found") ||
		strings.Contains(errStr, "does not exist") ||
		strings.Contains(errStr, "404")
}

// max returns the larger of two durations
func max(a, b time.Duration) time.Duration {
	if a > b {
		return a
	}
	return b
}

// getInstanceName generates the instance name following the pattern: appName-appVersion-instance
func getInstanceName(appName, appVersion string) string {
	return fmt.Sprintf("%s-%s-instance", appName, appVersion)
}
