package daemon

import (
	"context"
	"encoding/json"
	"strings"
	"time"
)

// RunCleanupLoop drives built-in Elastic Request Link cleanup while a Hub is
// serving Session Links. It closes expired grants, idle/draining links, and
// timed-out assignments without waiting for a future request to notice them.
func (manager *ElasticRequestLinkManager) RunCleanupLoop(ctx context.Context) {
	if manager == nil || ctx == nil {
		return
	}
	interval := manager.config.CleanupInterval
	if interval <= 0 {
		interval = defaultElasticRequestLinkCleanupInterval
	}
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			manager.CleanupExpired(manager.config.Now())
		}
	}
}

type ElasticRequestCleanupSummary struct {
	ExpiredGrants       int
	ClosedLinks         int
	CanceledAssignments int
}

// CleanupExpired is the synchronous half of the cleanup agentteam. It retires expired
// unconsumed grants, idle/draining/over-age links, and assignments whose
// per-request deadline elapsed, returning stable outcomes to waiting callers.
func (manager *ElasticRequestLinkManager) CleanupExpired(now time.Time) ElasticRequestCleanupSummary {
	if manager == nil {
		return ElasticRequestCleanupSummary{}
	}
	if now.IsZero() {
		now = manager.config.Now()
	}
	now = now.UTC()
	var toClose []elasticRequestLinkSender
	var assignments []*elasticRequestAssignmentState
	summary := ElasticRequestCleanupSummary{}
	manager.mu.Lock()
	manager.ensureWarmControllerLocked()
	for linkID, grantState := range manager.grants {
		if !grantState.consumed && now.After(grantState.grant.ExpiresAt) {
			if grantState.grant.Purpose == ElasticLinkPurposeWarm {
				manager.recordWarmFailureLocked(grantState.grant, "connect_timeout", now)
			}
			delete(manager.grants, linkID)
			summary.ExpiredGrants++
			manager.recordElasticEvent("cleanup_expired_grant", "expired")
		}
	}
	for linkID, link := range manager.links {
		if link.currentAssignment != "" {
			assignment := manager.assignmentsByID[link.currentAssignment]
			if assignment != nil && now.After(assignment.deadlineAt) {
				delete(manager.assignmentsByID, assignment.requestID)
				assignments = append(assignments, assignment)
				link.currentAssignment = ""
				link.lastIdleAt = now
				summary.CanceledAssignments++
				manager.recordElasticEvent("cleanup_canceled_assignment", "deadline_exceeded")
			}
			continue
		}
		if now.Sub(link.lastIdleAt) >= link.grant.IdleTimeout || now.Sub(link.openedAt) >= link.grant.MaxAge || link.draining {
			reason := "idle_timeout"
			if link.draining {
				reason = "drain"
			} else if now.Sub(link.openedAt) >= link.grant.MaxAge {
				reason = "max_age"
			}
			manager.removeLinkLocked(linkID)
			toClose = append(toClose, link.sender)
			summary.ClosedLinks++
			manager.recordElasticEvent("cleanup_closed_link", reason)
			if link.grant.Purpose == ElasticLinkPurposeWarm {
				manager.recordWarmEvent("user_interactive", link.grant.ProfileName, "closed", reason)
			}
		}
	}
	for leaseKey, lease := range manager.warm.leases {
		if !lease.Active(now) || manager.config.EmergencyDisabled {
			closeSenders, closedLinks := manager.drainSurplusWarmLinksLocked(lease.Key, lease.ProfileName, "lease_expired")
			toClose = append(toClose, closeSenders...)
			summary.ClosedLinks += closedLinks
			delete(manager.warm.leases, leaseKey)
		}
	}
	manager.recordPoolLocked()
	manager.mu.Unlock()
	for _, assignment := range assignments {
		closeAssignmentDone(assignment)
		result := RemoteRequestResult[json.RawMessage]{Type: sessionLinkRemoteRequestResultType, RequestID: assignment.requestID, RemoteRequestID: assignment.requestID, Success: false, Outcome: "deadline_exceeded", Error: "deadline_exceeded"}
		select {
		case assignment.resultCh <- result:
		default:
		}
	}
	for _, sender := range toClose {
		if sender != nil {
			_ = sender.Close()
		}
	}
	return summary
}

// CleanupSession tears down grants, links, and in-flight assignments bound to a
// machine/session generation when the Session Link is replaced or disconnected.
// This keeps Elastic Request Links sticky to the current same-node session.
func (manager *ElasticRequestLinkManager) CleanupSession(machineID string, sessionGeneration string, reason string) ElasticRequestCleanupSummary {
	if manager == nil {
		return ElasticRequestCleanupSummary{}
	}
	machineID = strings.TrimSpace(machineID)
	sessionGeneration = strings.TrimSpace(sessionGeneration)
	var toClose []elasticRequestLinkSender
	var assignments []*elasticRequestAssignmentState
	summary := ElasticRequestCleanupSummary{}
	manager.mu.Lock()
	manager.ensureWarmControllerLocked()
	for linkID, grantState := range manager.grants {
		grant := grantState.grant
		if grant.MachineID == machineID && (sessionGeneration == "" || grant.SessionGeneration == sessionGeneration) {
			delete(manager.grants, linkID)
			summary.ExpiredGrants++
		}
	}
	for linkID, link := range manager.links {
		grant := link.grant
		if grant.MachineID != machineID || (sessionGeneration != "" && grant.SessionGeneration != sessionGeneration) {
			continue
		}
		if link.currentAssignment != "" {
			if assignment := manager.assignmentsByID[link.currentAssignment]; assignment != nil {
				delete(manager.assignmentsByID, assignment.requestID)
				assignments = append(assignments, assignment)
				summary.CanceledAssignments++
				manager.recordElasticEvent("cleanup_canceled_assignment", reason)
			}
		}
		manager.removeLinkLocked(linkID)
		toClose = append(toClose, link.sender)
		summary.ClosedLinks++
		manager.recordElasticEvent("cleanup_closed_link", reason)
	}
	for leaseKey := range manager.warm.leases {
		if leaseKey.MachineID == machineID && (sessionGeneration == "" || leaseKey.SessionGeneration == sessionGeneration) {
			delete(manager.warm.leases, leaseKey)
		}
	}
	for cooldownKey := range manager.warm.cooldowns {
		if cooldownKey.MachineID == machineID && (sessionGeneration == "" || cooldownKey.SessionGeneration == sessionGeneration) {
			delete(manager.warm.cooldowns, cooldownKey)
		}
	}
	manager.recordPoolLocked()
	manager.mu.Unlock()
	for _, assignment := range assignments {
		closeAssignmentDone(assignment)
		result := RemoteRequestResult[json.RawMessage]{Type: sessionLinkRemoteRequestResultType, RequestID: assignment.requestID, RemoteRequestID: assignment.requestID, Success: false, Outcome: stableOutcomeForCleanup(reason), Error: stableOutcomeForCleanup(reason)}
		select {
		case assignment.resultCh <- result:
		default:
		}
	}
	for _, sender := range toClose {
		if sender != nil {
			_ = sender.Close()
		}
	}
	return summary
}

func (manager *ElasticRequestLinkManager) CloseLink(linkID string, reason string) bool {
	if manager == nil {
		return false
	}
	linkID = strings.TrimSpace(linkID)
	var sender elasticRequestLinkSender
	var assignment *elasticRequestAssignmentState
	manager.mu.Lock()
	link := manager.links[linkID]
	if link == nil {
		manager.mu.Unlock()
		return false
	}
	if link.currentAssignment != "" {
		assignment = manager.assignmentsByID[link.currentAssignment]
		if assignment != nil {
			delete(manager.assignmentsByID, assignment.requestID)
		}
	}
	sender = link.sender
	manager.removeLinkLocked(linkID)
	manager.recordElasticEvent("link_closed", reason)
	manager.recordPoolLocked()
	manager.mu.Unlock()
	if assignment != nil {
		closeAssignmentDone(assignment)
		result := RemoteRequestResult[json.RawMessage]{Type: sessionLinkRemoteRequestResultType, RequestID: assignment.requestID, RemoteRequestID: assignment.requestID, Success: false, Outcome: stableOutcomeForCleanup(reason), Error: stableOutcomeForCleanup(reason)}
		select {
		case assignment.resultCh <- result:
		default:
		}
	}
	if sender != nil {
		_ = sender.Close()
	}
	return true
}

func (manager *ElasticRequestLinkManager) DrainLink(linkID string, reason string) bool {
	if manager == nil {
		return false
	}
	manager.mu.Lock()
	link := manager.links[strings.TrimSpace(linkID)]
	if link == nil {
		manager.mu.Unlock()
		return false
	}
	link.draining = true
	idle := link.currentAssignment == ""
	sender := link.sender
	if idle {
		manager.removeLinkLocked(link.grant.LinkID)
	}
	manager.recordElasticEvent("link_drained", reason)
	manager.recordPoolLocked()
	manager.mu.Unlock()
	if idle && sender != nil {
		_ = sender.Close()
	}
	return true
}
