package service

import (
	"context"
	"fmt"
	"slices"
	"sync"
	"time"

	"security-center-backend/internal/pkg/lark"
	"security-center-backend/internal/pkg/logger"

	"github.com/robfig/cron/v3"
)

// RuntimeStatus represents the current state of the org sync runtime.
type RuntimeStatus struct {
	SchedulerRunning     bool       `json:"scheduler_running"`
	SyncRunning          bool       `json:"sync_running"`
	LastSyncAt           *time.Time `json:"last_sync_at,omitempty"`
	LastSuccessfulSyncAt *time.Time `json:"last_successful_sync_at,omitempty"`
	NextSyncAt           *time.Time `json:"next_sync_at,omitempty"`
	CurrentCron          string     `json:"current_cron,omitempty"`
	LarkConfigured       bool       `json:"lark_configured"`
	LastError            string     `json:"last_error,omitempty"`
	LastErrorAt          *time.Time `json:"last_error_at,omitempty"`
	FreshnessState       string     `json:"freshness_state"`
	TrustMode            string     `json:"trust_mode"`
	TrustedSnapshot      bool       `json:"trusted_snapshot"`
	SnapshotUsable       bool       `json:"snapshot_usable"`
	DegradedMode         bool       `json:"degraded_mode"`
	SnapshotAgeSeconds   *int64     `json:"snapshot_age_seconds,omitempty"`
	StaleAfterHours      int        `json:"stale_after_hours"`
	ExpiredAfterHours    int        `json:"expired_after_hours"`
	BlockingScopes       []string   `json:"blocking_scopes,omitempty"`
	BlockingReason       string     `json:"blocking_reason,omitempty"`
}

const (
	freshnessFresh       = "fresh"
	freshnessStale       = "stale"
	freshnessExpired     = "expired"
	freshnessNeverSynced = "never_synced"

	trustModeTrusted  = "trusted_snapshot"
	trustModeDegraded = "degraded_snapshot"
	trustModeBlocked  = "blocked"

	defaultStaleAfterHours   = 24
	defaultExpiredAfterHours = 72
)

type FreshnessPolicy struct {
	StaleAfterHours   int
	ExpiredAfterHours int
}

type syncFreshnessEvaluation struct {
	LastSuccessfulSyncAt *time.Time
	FreshnessState       string
	TrustMode            string
	TrustedSnapshot      bool
	SnapshotUsable       bool
	DegradedMode         bool
	SnapshotAgeSeconds   *int64
	BlockingScopes       []string
	BlockingReason       string
}

// OrgSyncRuntimeControl manages the lifecycle of the organization sync scheduler.
// It wraps the robfig/cron library with dynamic reconfiguration capabilities.
type OrgSyncRuntimeControl struct {
	deptSvc *DepartmentService
	logger  *logger.Logger

	mu            sync.RWMutex
	cronScheduler *cron.Cron
	cronEntryID   cron.EntryID
	isRunning     bool
	lastSyncAt    time.Time
	currentCron   string
	larkClient    *lark.Client
	lastError     string
	lastErrorAt   time.Time
	freshness     FreshnessPolicy
}

// NewOrgSyncRuntimeControl creates a new runtime control for org sync.
func NewOrgSyncRuntimeControl(deptSvc *DepartmentService, log *logger.Logger) *OrgSyncRuntimeControl {
	return &OrgSyncRuntimeControl{
		deptSvc:   deptSvc,
		logger:    log,
		freshness: DefaultFreshnessPolicy(),
	}
}

func DefaultFreshnessPolicy() FreshnessPolicy {
	return FreshnessPolicy{
		StaleAfterHours:   defaultStaleAfterHours,
		ExpiredAfterHours: defaultExpiredAfterHours,
	}
}

func NormalizeFreshnessPolicy(policy FreshnessPolicy) FreshnessPolicy {
	defaults := DefaultFreshnessPolicy()
	if policy.StaleAfterHours <= 0 {
		policy.StaleAfterHours = defaults.StaleAfterHours
	}
	if policy.ExpiredAfterHours <= 0 {
		policy.ExpiredAfterHours = defaults.ExpiredAfterHours
	}
	if policy.StaleAfterHours >= policy.ExpiredAfterHours {
		return defaults
	}
	return policy
}

func (p FreshnessPolicy) staleAfterDuration() time.Duration {
	return time.Duration(p.StaleAfterHours) * time.Hour
}

func (p FreshnessPolicy) expiredAfterDuration() time.Duration {
	return time.Duration(p.ExpiredAfterHours) * time.Hour
}

func (rc *OrgSyncRuntimeControl) SetFreshnessPolicy(policy FreshnessPolicy) RuntimeStatus {
	rc.mu.Lock()
	defer rc.mu.Unlock()

	rc.freshness = NormalizeFreshnessPolicy(policy)
	return rc.statusLocked()
}

// Start initializes and starts the cron scheduler with the provided configuration.
// It is idempotent - calling Start when already running will return current status.
func (rc *OrgSyncRuntimeControl) Start(cronExpr string, larkAppID string, larkAppSecret string) (RuntimeStatus, error) {
	rc.mu.Lock()
	defer rc.mu.Unlock()

	if rc.isRunning && rc.currentCron == cronExpr && rc.larkClient != nil {
		rc.logger.Info("org sync runtime already running with same config")
		return rc.statusLocked(), nil
	}

	// Validate cron expression
	if cronExpr == "" {
		return rc.statusLocked(), fmt.Errorf("cron expression cannot be empty")
	}

	// Validate Lark credentials
	if larkAppID == "" || larkAppSecret == "" {
		return rc.statusLocked(), fmt.Errorf("lark credentials are required")
	}

	// Stop existing scheduler if running with different config
	if rc.isRunning {
		rc.stopLocked()
	}

	// Initialize Lark client
	rc.larkClient = lark.NewClient(larkAppID, larkAppSecret)
	rc.deptSvc.SetLarkClient(rc.larkClient)

	// Create new cron scheduler
	rc.cronScheduler = cron.New()

	// Add sync job to scheduler
	entryID, err := rc.cronScheduler.AddFunc(cronExpr, func() {
		rc.executeSyncJob()
	})
	if err != nil {
		rc.lastError = err.Error()
		rc.lastErrorAt = time.Now().UTC()
		return rc.statusLocked(), fmt.Errorf("failed to schedule sync job: %w", err)
	}

	rc.cronEntryID = entryID
	rc.currentCron = cronExpr
	rc.cronScheduler.Start()
	rc.isRunning = true
	rc.lastError = ""
	rc.lastErrorAt = time.Time{}

	rc.logger.Info("org sync runtime started", "cron", cronExpr)
	return rc.statusLocked(), nil
}

// Stop halts the cron scheduler gracefully.
func (rc *OrgSyncRuntimeControl) Stop() RuntimeStatus {
	rc.mu.Lock()
	defer rc.mu.Unlock()
	return rc.stopLocked()
}

// stopLocked is the internal stop implementation (caller must hold lock).
func (rc *OrgSyncRuntimeControl) stopLocked() RuntimeStatus {
	if !rc.isRunning {
		return rc.statusLocked()
	}

	if rc.cronScheduler != nil {
		ctx := rc.cronScheduler.Stop()
		// Wait for running jobs to complete (with timeout)
		select {
		case <-ctx.Done():
			rc.logger.Info("org sync scheduler stopped gracefully")
		case <-time.After(30 * time.Second):
			rc.logger.Warn("org sync scheduler stop timed out")
		}
		rc.cronScheduler = nil
	}

	rc.isRunning = false
	rc.cronEntryID = 0
	rc.currentCron = ""
	rc.larkClient = nil

	rc.logger.Info("org sync runtime stopped")
	return rc.statusLocked()
}

// Reconfigure stops the current scheduler and starts a new one with updated configuration.
// This enables dynamic reconfiguration without server restart.
func (rc *OrgSyncRuntimeControl) Reconfigure(cronExpr string, larkAppID string, larkAppSecret string) (RuntimeStatus, error) {
	rc.mu.Lock()
	defer rc.mu.Unlock()

	rc.logger.Info("reconfiguring org sync runtime", "old_cron", rc.currentCron, "new_cron", cronExpr)

	// Stop current scheduler
	if rc.isRunning {
		rc.stopLocked()
	}

	// Validate new configuration
	if cronExpr == "" || larkAppID == "" || larkAppSecret == "" {
		return rc.statusLocked(), fmt.Errorf("invalid configuration: all fields are required")
	}

	// Initialize new Lark client
	rc.larkClient = lark.NewClient(larkAppID, larkAppSecret)
	rc.deptSvc.SetLarkClient(rc.larkClient)

	// Create new cron scheduler
	rc.cronScheduler = cron.New()

	entryID, err := rc.cronScheduler.AddFunc(cronExpr, func() {
		rc.executeSyncJob()
	})
	if err != nil {
		rc.lastError = err.Error()
		rc.lastErrorAt = time.Now().UTC()
		return rc.statusLocked(), fmt.Errorf("failed to schedule sync job: %w", err)
	}

	rc.cronEntryID = entryID
	rc.currentCron = cronExpr
	rc.cronScheduler.Start()
	rc.isRunning = true
	rc.lastError = ""
	rc.lastErrorAt = time.Time{}

	rc.logger.Info("org sync runtime reconfigured successfully", "cron", cronExpr)
	return rc.statusLocked(), nil
}

// Status returns the current runtime status.
func (rc *OrgSyncRuntimeControl) Status() RuntimeStatus {
	rc.mu.RLock()
	status := rc.statusLocked()
	rc.mu.RUnlock()

	return rc.applyFreshness(status)
}

// statusLocked returns the current status (caller must hold lock).
func (rc *OrgSyncRuntimeControl) statusLocked() RuntimeStatus {
	policy := NormalizeFreshnessPolicy(rc.freshness)
	status := RuntimeStatus{
		SchedulerRunning:  rc.isRunning,
		CurrentCron:       rc.currentCron,
		LarkConfigured:    rc.larkClient != nil,
		LastError:         rc.lastError,
		SyncRunning:       rc.deptSvc != nil && rc.deptSvc.IsSyncRunning(),
		FreshnessState:    freshnessNeverSynced,
		TrustMode:         trustModeBlocked,
		SnapshotUsable:    false,
		StaleAfterHours:   policy.StaleAfterHours,
		ExpiredAfterHours: policy.ExpiredAfterHours,
	}

	// Calculate next run time if scheduler is running
	if rc.isRunning && rc.cronScheduler != nil && rc.cronEntryID != 0 {
		entry := rc.cronScheduler.Entry(rc.cronEntryID)
		if !entry.Next.IsZero() {
			next := entry.Next
			status.NextSyncAt = &next
		}
	}

	if !rc.lastSyncAt.IsZero() {
		last := rc.lastSyncAt
		status.LastSyncAt = &last
	}
	if !rc.lastErrorAt.IsZero() {
		errAt := rc.lastErrorAt
		status.LastErrorAt = &errAt
	}

	return status
}

func (rc *OrgSyncRuntimeControl) applyFreshness(status RuntimeStatus) RuntimeStatus {
	if rc.deptSvc == nil {
		status.BlockingReason = "org sync service is not configured"
		status.BlockingScopes = []string{"picker_reads", "team_fanout", "department_fanout"}
		return status
	}

	policy := NormalizeFreshnessPolicy(FreshnessPolicy{
		StaleAfterHours:   status.StaleAfterHours,
		ExpiredAfterHours: status.ExpiredAfterHours,
	})
	eval, err := rc.evaluateFreshness(context.Background(), time.Now().UTC(), policy)
	if err != nil {
		if rc.logger != nil {
			rc.logger.Warn("failed to evaluate org sync freshness", "error", err)
		}
		status.BlockingReason = "failed to evaluate org sync freshness"
		status.BlockingScopes = []string{"team_fanout", "department_fanout"}
		return status
	}

	status.LastSuccessfulSyncAt = eval.LastSuccessfulSyncAt
	status.FreshnessState = eval.FreshnessState
	status.TrustMode = eval.TrustMode
	status.TrustedSnapshot = eval.TrustedSnapshot
	status.SnapshotUsable = eval.SnapshotUsable
	status.DegradedMode = eval.DegradedMode
	status.SnapshotAgeSeconds = eval.SnapshotAgeSeconds
	status.BlockingScopes = eval.BlockingScopes
	status.BlockingReason = eval.BlockingReason
	return status
}

func (rc *OrgSyncRuntimeControl) evaluateFreshness(ctx context.Context, now time.Time, policy FreshnessPolicy) (syncFreshnessEvaluation, error) {
	eval := syncFreshnessEvaluation{
		FreshnessState: freshnessNeverSynced,
		TrustMode:      trustModeBlocked,
		SnapshotUsable: false,
		DegradedMode:   true,
		BlockingScopes: []string{"picker_reads", "team_fanout", "department_fanout"},
		BlockingReason: "no successful Feishu Org Sync has completed yet",
	}

	run, err := rc.deptSvc.LatestSuccessfulSync(ctx)
	if err != nil {
		return eval, err
	}
	if run == nil || run.FinishedAt == nil {
		return eval, nil
	}

	lastSuccessful := run.FinishedAt.UTC()
	age := now.Sub(lastSuccessful)
	ageSeconds := int64(age.Seconds())
	if ageSeconds < 0 {
		ageSeconds = 0
	}
	eval.LastSuccessfulSyncAt = &lastSuccessful
	eval.SnapshotAgeSeconds = &ageSeconds

	switch {
	case age <= policy.staleAfterDuration():
		eval.FreshnessState = freshnessFresh
		eval.TrustMode = trustModeTrusted
		eval.TrustedSnapshot = true
		eval.SnapshotUsable = true
		eval.DegradedMode = false
		eval.BlockingScopes = nil
		eval.BlockingReason = ""
	case age <= policy.expiredAfterDuration():
		eval.FreshnessState = freshnessStale
		eval.TrustMode = trustModeDegraded
		eval.TrustedSnapshot = false
		eval.SnapshotUsable = true
		eval.DegradedMode = true
		eval.BlockingScopes = []string{"team_fanout", "department_fanout"}
		eval.BlockingReason = "directory snapshot is stale; keep picker/read flows on the last good snapshot but stop org-aware fan-out"
	default:
		eval.FreshnessState = freshnessExpired
		eval.TrustMode = trustModeBlocked
		eval.TrustedSnapshot = false
		eval.SnapshotUsable = false
		eval.DegradedMode = true
		eval.BlockingScopes = []string{"picker_reads", "team_fanout", "department_fanout"}
		eval.BlockingReason = "directory snapshot is expired; require a successful Org Sync before trusting picker or org-aware routing data"
	}
	if len(eval.BlockingScopes) > 0 {
		slices.Sort(eval.BlockingScopes)
	}
	return eval, nil
}

// executeSyncJob is the function called by the cron scheduler.
func (rc *OrgSyncRuntimeControl) executeSyncJob() {
	rc.logger.Info("starting scheduled org sync")

	// Execute sync with timeout
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

	if _, err := rc.deptSvc.SyncFromLarkScheduled(ctx); err != nil {
		rc.logger.Error("scheduled org sync failed", "error", err)
		rc.mu.Lock()
		rc.lastError = err.Error()
		rc.lastErrorAt = time.Now().UTC()
		rc.mu.Unlock()
		return
	}

	rc.mu.Lock()
	rc.lastSyncAt = time.Now().UTC()
	rc.lastError = ""
	rc.lastErrorAt = time.Time{}
	rc.mu.Unlock()

	rc.logger.Info("scheduled org sync completed successfully")
}
