package runtimedeploy

import (
	"context"
	"errors"
	"fmt"
	"path/filepath"
	"strings"
	"time"

	"github.com/codex-k8s/kodex/libs/go/servicescfg"
	runtimedeploytaskrepo "github.com/codex-k8s/kodex/services/internal/control-plane/internal/domain/repository/runtimedeploytask"
	entitytypes "github.com/codex-k8s/kodex/services/internal/control-plane/internal/domain/types/entity"
)

// ReconcileNext claims one pending deploy task and applies desired state.
func (s *Service) ReconcileNext(ctx context.Context, leaseOwner string, leaseTTL time.Duration) (bool, error) {
	leaseOwner = strings.TrimSpace(leaseOwner)
	if leaseOwner == "" {
		return false, fmt.Errorf("runtime deploy reconciler lease owner is required")
	}
	if leaseTTL < time.Second {
		leaseTTL = time.Second
	}

	renewInterval := runtimeDeployLeaseRenewInterval(leaseTTL)
	staleRunningTimeout := runtimeDeployStaleRunningTimeout(renewInterval)
	leaseTTLString := fmt.Sprintf("%d seconds", int64(leaseTTL.Seconds()))
	staleRunningTimeoutString := fmt.Sprintf("%d seconds", int64(staleRunningTimeout.Seconds()))
	task, ok, err := s.tasks.ClaimNext(ctx, runtimedeploytaskrepo.ClaimParams{
		LeaseOwner:          leaseOwner,
		LeaseTTL:            leaseTTLString,
		StaleRunningTimeout: staleRunningTimeoutString,
	})
	if err != nil {
		return false, fmt.Errorf("claim runtime deploy task: %w", err)
	}
	if !ok {
		return false, nil
	}
	s.appendTaskLogBestEffort(ctx, task.RunID, "reconcile", "info", "Task claimed by reconciler "+leaseOwner)

	renewCtx, cancelRenew := context.WithCancel(ctx)
	renewDone := make(chan struct{})
	go func() {
		defer close(renewDone)

		// Keep the lease short for fast recovery when the reconciler dies during self-deploy,
		// but renew it while we are actively processing the task.
		ticker := time.NewTicker(renewInterval)
		defer ticker.Stop()

		for {
			select {
			case <-renewCtx.Done():
				return
			case <-ticker.C:
				updated, err := s.tasks.RenewLease(renewCtx, runtimedeploytaskrepo.RenewLeaseParams{
					RunID:      task.RunID,
					LeaseOwner: leaseOwner,
					LeaseTTL:   leaseTTLString,
				})
				if err != nil {
					s.logger.Error("renew runtime deploy task lease failed", "run_id", task.RunID, "lease_owner", leaseOwner, "err", err)
					continue
				}
				if !updated {
					s.logger.Warn("runtime deploy task lease lost while renewing", "run_id", task.RunID, "lease_owner", leaseOwner)
					cancelRenew()
					return
				}
			}
		}
	}()
	cancelWatchDone := make(chan struct{})
	go func() {
		defer close(cancelWatchDone)

		ticker := time.NewTicker(time.Second)
		defer ticker.Stop()

		for {
			select {
			case <-renewCtx.Done():
				return
			case <-ticker.C:
				if !s.isTaskCanceled(ctx, task.RunID) {
					continue
				}
				s.logger.Info("runtime deploy task canceled while running", "run_id", task.RunID, "lease_owner", leaseOwner)
				cancelRenew()
				return
			}
		}
	}()

	result, runErr := s.applyDesiredState(renewCtx, PrepareParams{
		RunID:              task.RunID,
		RuntimeMode:        task.RuntimeMode,
		Namespace:          task.Namespace,
		TargetEnv:          task.TargetEnv,
		SlotNo:             task.SlotNo,
		RepositoryFullName: task.RepositoryFullName,
		ServicesYAMLPath:   task.ServicesYAMLPath,
		BuildRef:           task.BuildRef,
		DeployOnly:         task.DeployOnly,
	})
	cancelRenew()
	<-renewDone
	<-cancelWatchDone
	if runErr != nil {
		s.appendTaskLogBestEffort(ctx, task.RunID, "reconcile", "error", "Task failed: "+runErr.Error())
		if errors.Is(runErr, context.Canceled) || errors.Is(runErr, context.DeadlineExceeded) {
			if ctx.Err() != nil {
				requeueMessage := "Reconciler shutdown detected while task was running; requeued for another instance"
				s.appendTaskLogBestEffort(ctx, task.RunID, "reconcile", "warning", requeueMessage)
				requeueCtx, cancelRequeue := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
				requeued, requeueErr := s.tasks.Requeue(requeueCtx, runtimedeploytaskrepo.RequeueParams{
					RunID:      task.RunID,
					LeaseOwner: leaseOwner,
					LastError:  requeueMessage,
				})
				cancelRequeue()
				if requeueErr != nil {
					return true, fmt.Errorf("requeue runtime deploy task %s after shutdown: %w", task.RunID, requeueErr)
				}
				if !requeued {
					s.logger.Warn("runtime deploy task requeue skipped after shutdown (lease lost)", "run_id", task.RunID, "lease_owner", leaseOwner)
				}
				return true, nil
			}
			if s.isTaskCanceled(ctx, task.RunID) {
				s.appendTaskLogBestEffort(ctx, task.RunID, "reconcile", "info", "Task canceled because newer deploy superseded current one")
				return true, nil
			}
		}
		lastError := strings.TrimSpace(runErr.Error())
		if len(lastError) > 4000 {
			lastError = lastError[:4000]
		}
		updated, markErr := s.tasks.MarkFailed(ctx, runtimedeploytaskrepo.MarkFailedParams{
			RunID:      task.RunID,
			LeaseOwner: leaseOwner,
			LastError:  lastError,
		})
		if markErr != nil {
			return true, fmt.Errorf("mark runtime deploy task %s as failed: %w", task.RunID, markErr)
		}
		if !updated {
			if s.isTaskCanceled(ctx, task.RunID) {
				s.appendTaskLogBestEffort(ctx, task.RunID, "reconcile", "info", "Task canceled before failed mark commit")
				return true, nil
			}
			return true, fmt.Errorf("mark runtime deploy task %s as failed: lease lost", task.RunID)
		}
		return true, nil
	}

	updated, err := s.tasks.MarkSucceeded(ctx, runtimedeploytaskrepo.MarkSucceededParams{
		RunID:           task.RunID,
		LeaseOwner:      leaseOwner,
		ResultNamespace: result.Namespace,
		ResultTargetEnv: result.TargetEnv,
	})
	if err != nil {
		return true, fmt.Errorf("mark runtime deploy task %s as succeeded: %w", task.RunID, err)
	}
	if !updated {
		if s.isTaskCanceled(ctx, task.RunID) {
			s.appendTaskLogBestEffort(ctx, task.RunID, "reconcile", "info", "Task result ignored because task was canceled")
			return true, nil
		}
		return true, fmt.Errorf("mark runtime deploy task %s as succeeded: lease lost", task.RunID)
	}
	s.appendTaskLogBestEffort(ctx, task.RunID, "reconcile", "info", "Task succeeded for namespace "+result.Namespace+" env "+result.TargetEnv)
	return true, nil
}

func (s *Service) isTaskCanceled(ctx context.Context, runID string) bool {
	task, ok, err := s.tasks.GetByRunID(ctx, runID)
	if err != nil {
		s.logger.Warn("load runtime deploy task status for cancellation check failed", "run_id", runID, "err", err)
		return false
	}
	if !ok {
		return false
	}
	return task.Status == entitytypes.RuntimeDeployTaskStatusCanceled
}

func runtimeDeployLeaseRenewInterval(leaseTTL time.Duration) time.Duration {
	interval := leaseTTL / 2
	if interval > 30*time.Second {
		interval = 30 * time.Second
	}
	if interval < time.Second {
		interval = time.Second
	}
	return interval
}

func runtimeDeployStaleRunningTimeout(renewInterval time.Duration) time.Duration {
	timeout := renewInterval*2 + 5*time.Second
	if timeout < 30*time.Second {
		return 30 * time.Second
	}
	if timeout > 2*time.Minute {
		return 2 * time.Minute
	}
	return timeout
}

// applyDesiredState builds images and applies infrastructure/services for one runtime target namespace.
func (s *Service) applyDesiredState(ctx context.Context, params PrepareParams) (PrepareResult, error) {
	params = normalizePrepareParams(params)
	zero := PrepareResult{}
	runID := strings.TrimSpace(params.RunID)
	if runID == "" {
		return zero, fmt.Errorf("run_id is required")
	}
	s.appendTaskLogBestEffort(ctx, runID, "prepare", "info", "Start runtime deploy applyDesiredState")

	targetEnv := strings.TrimSpace(params.TargetEnv)
	if targetEnv == "" {
		targetEnv = "ai"
	}
	targetNamespace := strings.TrimSpace(params.Namespace)
	templateVars := s.buildTemplateVars(params, targetNamespace)
	repositoryRoot, err := s.resolveRunRepositoryRoot(ctx, params, templateVars, runID)
	if err != nil {
		s.appendTaskLogBestEffort(ctx, runID, "repo-sync", "error", "Resolve repository snapshot failed: "+err.Error())
		return zero, fmt.Errorf("resolve repository snapshot: %w", err)
	}
	servicesConfigPath := s.resolveServicesConfigPath(repositoryRoot, params.ServicesYAMLPath)
	loaded, err := servicescfg.Load(servicesConfigPath, servicescfg.LoadOptions{
		Env:       targetEnv,
		Namespace: targetNamespace,
		Slot:      params.SlotNo,
		Vars:      templateVars,
	})
	if err != nil {
		s.appendTaskLogBestEffort(ctx, runID, "prepare", "error", "Load services config failed: "+err.Error())
		return zero, fmt.Errorf("load services config: %w", err)
	}

	if strings.TrimSpace(targetNamespace) == "" {
		targetNamespace = strings.TrimSpace(loaded.Context.Namespace)
	}
	if targetNamespace == "" {
		return zero, fmt.Errorf("resolved target namespace is empty")
	}
	if effectiveEnv := strings.TrimSpace(loaded.Context.Env); effectiveEnv != "" {
		targetEnv = effectiveEnv
		params.TargetEnv = targetEnv
	}

	// Template vars are used to render Kubernetes manifests. Some variables depend on
	// the final namespace and must be (re)computed after services.yaml resolved it.
	templateVars = s.buildTemplateVars(params, targetNamespace)
	applyStackImageVars(templateVars, loaded.Stack)

	templateVars["KODEX_PRODUCTION_NAMESPACE"] = targetNamespace
	templateVars["KODEX_WORKER_K8S_NAMESPACE"] = targetNamespace
	templateVars["KODEX_REPOSITORY_ROOT"] = s.repositoryRootForRuntimeEnv(repositoryRoot)
	if repoName := strings.TrimSpace(params.RepositoryFullName); repoName != "" {
		templateVars["KODEX_GITHUB_REPO"] = repoName
	}
	if err := s.ensureRuntimeNamespaceRepoSnapshot(ctx, params, targetEnv, targetNamespace, repositoryRoot, templateVars, runID); err != nil {
		s.appendTaskLogBestEffort(ctx, runID, "repo-sync", "error", "Ensure runtime namespace repo snapshot failed: "+err.Error())
		return zero, fmt.Errorf("ensure runtime namespace repo snapshot: %w", err)
	}
	if strings.TrimSpace(templateVars["KODEX_WORKER_JOB_IMAGE"]) == "" {
		if value := strings.TrimSpace(templateVars["KODEX_AGENT_RUNNER_IMAGE"]); value != "" {
			templateVars["KODEX_WORKER_JOB_IMAGE"] = value
		}
	}

	// Allow services.yaml to override public host resolution (full-env domainTemplate).
	applyEnvironmentDomainTemplate(templateVars, loaded.Stack, targetEnv)

	if strings.EqualFold(strings.TrimSpace(loaded.Stack.Spec.Project), "kodex") {
		s.appendTaskLogBestEffort(ctx, runID, "prerequisites", "info", "Ensuring kodex prerequisites")
		if err := s.ensureCodexK8sPrerequisites(ctx, repositoryRoot, targetNamespace, templateVars, loaded.Stack, runID); err != nil {
			s.appendTaskLogBestEffort(ctx, runID, "prerequisites", "error", "Ensure prerequisites failed: "+err.Error())
			return zero, fmt.Errorf("ensure kodex prerequisites: %w", err)
		}
	}

	issuerBefore := strings.TrimSpace(templateVars["KODEX_CERT_ISSUER_ENABLED"])
	if err := s.prepareTLS(ctx, repositoryRoot, targetEnv, targetNamespace, templateVars, runID); err != nil {
		s.appendTaskLogBestEffort(ctx, runID, "tls", "error", "Prepare TLS failed: "+err.Error())
		return zero, fmt.Errorf("prepare tls: %w", err)
	}
	if strings.TrimSpace(templateVars["KODEX_CERT_ISSUER_ENABLED"]) != issuerBefore {
		reloaded, err := servicescfg.Load(servicesConfigPath, servicescfg.LoadOptions{
			Env:       targetEnv,
			Namespace: targetNamespace,
			Slot:      params.SlotNo,
			Vars:      templateVars,
		})
		if err != nil {
			s.appendTaskLogBestEffort(ctx, runID, "prepare", "error", "Reload services config after TLS update failed: "+err.Error())
			return zero, fmt.Errorf("reload services config after tls update: %w", err)
		}
		loaded = reloaded
	}

	if _, err := s.applyInfrastructure(ctx, repositoryRoot, loaded.Stack, targetNamespace, templateVars, runID); err != nil {
		s.appendTaskLogBestEffort(ctx, runID, "infrastructure", "error", "Apply infrastructure failed: "+err.Error())
		return zero, fmt.Errorf("apply infrastructure: %w", err)
	}
	if err := s.buildImages(ctx, repositoryRoot, params, loaded.Stack, targetNamespace, templateVars); err != nil {
		s.appendTaskLogBestEffort(ctx, runID, "build", "error", "Build images failed: "+err.Error())
		return zero, fmt.Errorf("build images: %w", err)
	}
	appliedInfra, err := s.applyInfrastructure(ctx, repositoryRoot, loaded.Stack, targetNamespace, templateVars, runID)
	if err != nil {
		s.appendTaskLogBestEffort(ctx, runID, "infrastructure", "error", "Re-apply infrastructure failed: "+err.Error())
		return zero, fmt.Errorf("re-apply infrastructure: %w", err)
	}
	if err := s.applyServices(ctx, repositoryRoot, loaded.Stack, targetNamespace, templateVars, appliedInfra, runID); err != nil {
		s.appendTaskLogBestEffort(ctx, runID, "services", "error", "Apply services failed: "+err.Error())
		return zero, fmt.Errorf("apply services: %w", err)
	}

	if err := s.finalizeTLS(ctx, targetEnv, targetNamespace, templateVars, runID); err != nil {
		s.appendTaskLogBestEffort(ctx, runID, "tls", "error", "Finalize TLS failed: "+err.Error())
		return zero, fmt.Errorf("finalize tls: %w", err)
	}
	if fingerprint, fingerprintErr := s.buildRuntimeFingerprint(ctx, EvaluateReuseParams{
		RunID:              params.RunID,
		RuntimeMode:        params.RuntimeMode,
		Namespace:          targetNamespace,
		TargetEnv:          targetEnv,
		SlotNo:             params.SlotNo,
		RepositoryFullName: params.RepositoryFullName,
		ServicesYAMLPath:   params.ServicesYAMLPath,
		BuildRef:           params.BuildRef,
		DeployOnly:         params.DeployOnly,
	}); fingerprintErr == nil {
		if err := s.persistRuntimeFingerprint(ctx, targetNamespace, fingerprint); err != nil {
			s.appendTaskLogBestEffort(ctx, runID, "prepare", "warning", "Persist runtime fingerprint failed: "+err.Error())
		}
	} else {
		s.appendTaskLogBestEffort(ctx, runID, "prepare", "warning", "Build runtime fingerprint skipped: "+fingerprintErr.Error())
	}
	s.appendTaskLogBestEffort(ctx, runID, "prepare", "info", "Runtime deploy finished successfully")
	return PrepareResult{
		Namespace: targetNamespace,
		TargetEnv: targetEnv,
	}, nil
}

func (s *Service) repositoryRootForRuntimeEnv(resolvedRepositoryRoot string) string {
	if configured := strings.TrimSpace(s.cfg.RepositoryRoot); configured != "" {
		return normalizeRepositoryCacheRoot(configured)
	}
	return normalizeRepositoryCacheRoot(resolvedRepositoryRoot)
}

func normalizeRepositoryCacheRoot(path string) string {
	path = strings.TrimSpace(path)
	if path == "" {
		return ""
	}
	cleanPath := filepath.Clean(path)
	marker := string(filepath.Separator) + "github" + string(filepath.Separator)
	idx := strings.Index(cleanPath, marker)
	if idx < 0 {
		return cleanPath
	}
	rest := strings.Trim(cleanPath[idx+len(marker):], string(filepath.Separator))
	if rest == "" {
		return cleanPath
	}
	parts := strings.Split(rest, string(filepath.Separator))
	// Snapshot layout: <cacheRoot>/github/<owner>/<repo>/<ref>.
	// If the path matches this (or deeper), keep only <cacheRoot>.
	if len(parts) < 3 {
		return cleanPath
	}
	root := strings.TrimRight(cleanPath[:idx], string(filepath.Separator))
	if root == "" {
		return string(filepath.Separator)
	}
	return root
}
