package service

import (
	"sync"
	"sync/atomic"
	"time"

	"github.com/google/uuid"
	"github.com/phishingclub/phishingclub/database"
	"github.com/phishingclub/phishingclub/model"
	"go.uber.org/zap"
)

// ProxySession represents an active MITM proxy session
type ProxySession struct {
	ID                    string
	CampaignRecipientID   *uuid.UUID
	CampaignID            *uuid.UUID
	RecipientID           *uuid.UUID
	Campaign              *model.Campaign
	Domain                *database.Domain
	TargetDomain          string
	Config                sync.Map // map[string]ProxyServiceDomainConfig
	CreatedAt             time.Time
	RequiredCaptures      sync.Map     // map[string]bool
	CapturedData          sync.Map     // map[string]map[string]string
	NextPageType          atomic.Value // string - accessed concurrently by multiple requests
	IsComplete            atomic.Bool  // accessed concurrently when checking capture completion
	CookieBundleSubmitted atomic.Bool  // accessed concurrently to prevent duplicate submissions

	// client user-agent stored for analytics and logging
	UserAgent string
}

// ProxySessionManager manages proxy session lifecycle and storage
type ProxySessionManager struct {
	Common
	sessions                  sync.Map // map[sessionID]*ProxySession
	campaignRecipientSessions sync.Map // map[campaignRecipientID]sessionID
	urlMappings               sync.Map // map[rewritten URL]original URL
	queryParameterMappings    sync.Map // map[rewritten path][]ProxyServiceURLRewriteQueryParam
}

// NewProxySessionManager creates a new proxy session manager
func NewProxySessionManager(logger *zap.SugaredLogger) *ProxySessionManager {
	return &ProxySessionManager{
		Common: Common{
			Logger: logger,
		},
	}
}

// GetSession retrieves a session by ID
func (m *ProxySessionManager) GetSession(sessionID string) (*ProxySession, bool) {
	val, ok := m.sessions.Load(sessionID)
	if !ok {
		return nil, false
	}
	session, ok := val.(*ProxySession)
	return session, ok
}

// StoreSession stores a session
func (m *ProxySessionManager) StoreSession(sessionID string, session *ProxySession) {
	m.sessions.Store(sessionID, session)
}

// DeleteSession deletes a session and its associated campaign recipient mapping
func (m *ProxySessionManager) DeleteSession(sessionID string) {
	if val, ok := m.sessions.Load(sessionID); ok {
		if session, ok := val.(*ProxySession); ok {
			if session.CampaignRecipientID != nil {
				m.campaignRecipientSessions.Delete(session.CampaignRecipientID.String())
			}
		}
		m.sessions.Delete(sessionID)
	}
}

// GetSessionByCampaignRecipient retrieves a session ID by campaign recipient ID
func (m *ProxySessionManager) GetSessionByCampaignRecipient(campaignRecipientID string) (string, bool) {
	val, ok := m.campaignRecipientSessions.Load(campaignRecipientID)
	if !ok {
		return "", false
	}
	sessionID, ok := val.(string)
	return sessionID, ok
}

// StoreCampaignRecipientSession stores the mapping between campaign recipient and session
func (m *ProxySessionManager) StoreCampaignRecipientSession(campaignRecipientID string, sessionID string) {
	m.campaignRecipientSessions.Store(campaignRecipientID, sessionID)
}

// StoreURLMapping stores a URL mapping for rewrite tracking
func (m *ProxySessionManager) StoreURLMapping(rewrittenURL string, originalURL string) {
	m.urlMappings.Store(rewrittenURL, originalURL)
}

// GetURLMapping retrieves the original URL for a rewritten URL
func (m *ProxySessionManager) GetURLMapping(rewrittenURL string) (string, bool) {
	val, ok := m.urlMappings.Load(rewrittenURL)
	if !ok {
		return "", false
	}
	originalURL, ok := val.(string)
	return originalURL, ok
}

// StoreQueryParameterMappings stores query parameter mappings for reverse lookup
func (m *ProxySessionManager) StoreQueryParameterMappings(rewrittenPath string, queryRules []ProxyServiceURLRewriteQueryParam) {
	m.queryParameterMappings.Store(rewrittenPath, queryRules)
}

// GetQueryParameterMappings retrieves query parameter mappings for a rewritten path
func (m *ProxySessionManager) GetQueryParameterMappings(rewrittenPath string) ([]ProxyServiceURLRewriteQueryParam, bool) {
	val, ok := m.queryParameterMappings.Load(rewrittenPath)
	if !ok {
		return nil, false
	}
	queryRules, ok := val.([]ProxyServiceURLRewriteQueryParam)
	return queryRules, ok
}

// RangeSessions iterates over all sessions
func (m *ProxySessionManager) RangeSessions(fn func(sessionID string, session *ProxySession) bool) {
	m.sessions.Range(func(key, value interface{}) bool {
		sessionID, ok := key.(string)
		if !ok {
			return true
		}
		session, ok := value.(*ProxySession)
		if !ok {
			return true
		}
		return fn(sessionID, session)
	})
}

// ClearSessionsForProxy clears all sessions associated with a proxy configuration
func (m *ProxySessionManager) ClearSessionsForProxy(proxyID string) {
	if proxyID == "" {
		return
	}

	clearedCount := 0

	m.sessions.Range(func(key, value interface{}) bool {
		sessionID, ok := key.(string)
		if !ok {
			return true
		}
		session, ok := value.(*ProxySession)
		if !ok {
			return true
		}

		// check if this session's domain belongs to the proxy
		if session.Domain != nil && session.Domain.ProxyID != nil {
			if session.Domain.ProxyID.String() == proxyID {
				m.DeleteSession(sessionID)
				clearedCount++
				m.Logger.Debugw("cleared session for proxy",
					"sessionID", sessionID,
					"proxyID", proxyID,
					"domain", session.Domain.Name,
				)
			}
		}
		return true
	})

	if clearedCount > 0 {
		m.Logger.Infow("cleared all sessions for proxy",
			"count", clearedCount,
			"proxyID", proxyID,
		)
	}
}

// ClearSessionsForDomains clears all sessions associated with specific phishing domains
func (m *ProxySessionManager) ClearSessionsForDomains(phishingDomains []string) {
	if len(phishingDomains) == 0 {
		return
	}

	// create a map for fast lookup
	domainMap := make(map[string]bool)
	for _, domain := range phishingDomains {
		domainMap[domain] = true
	}

	clearedCount := 0

	m.sessions.Range(func(key, value interface{}) bool {
		sessionID, ok := key.(string)
		if !ok {
			return true
		}
		session, ok := value.(*ProxySession)
		if !ok {
			return true
		}

		// check if this session's domain matches any of the affected domains
		if session.Domain != nil {
			domainName := session.Domain.Name
			if domainMap[domainName] {
				m.DeleteSession(sessionID)
				clearedCount++
				m.Logger.Debugw("cleared session for affected domain",
					"sessionID", sessionID,
					"domain", domainName,
				)
			}
		}
		return true
	})

	if clearedCount > 0 {
		m.Logger.Infow("cleared sessions for affected domains",
			"count", clearedCount,
			"domains", phishingDomains,
		)
	}
}

// CleanupExpiredSessions removes sessions older than maxAge
func (m *ProxySessionManager) CleanupExpiredSessions(maxAge time.Duration) int {
	now := time.Now()
	cleanedCount := 0

	m.sessions.Range(func(key, value interface{}) bool {
		sessionID, ok := key.(string)
		if !ok {
			return true
		}
		session, ok := value.(*ProxySession)
		if !ok {
			m.sessions.Delete(sessionID)
			cleanedCount++
			return true
		}

		sessionAge := now.Sub(session.CreatedAt)
		if sessionAge > maxAge {
			m.DeleteSession(sessionID)
			cleanedCount++
		}
		return true
	})

	if cleanedCount > 0 {
		m.Logger.Debugw("cleaned up expired sessions", "count", cleanedCount)
	}

	return cleanedCount
}
