package audit

import (
	"context"
	"strings"
	"sync"
	"time"

	"security-center-backend/internal/model"
	"security-center-backend/internal/pkg/logger"
	"security-center-backend/internal/repository"
	"security-center-backend/internal/service/directory"
)

type Entry struct {
	UserID       *uint64
	ActorType    string
	ActorUserID  *uint64
	ActorEmail   string
	Action       string
	ResourceType string
	ResourceID   string
	Metadata     model.JSONB
	BeforeState  model.JSONB
	AfterState   model.JSONB
	IPAddress    string
	UserAgent    string
}

type Service struct {
	repo         repository.AuditRepository
	userRepo     userFinder
	employeeRepo directory.EmployeeDirectory
	queue        chan *model.AuditLog
	stop         chan struct{}
	once         sync.Once
	wg           sync.WaitGroup
	logger       *logger.Logger
	userExists   sync.Map
}

type userFinder interface {
	FindByID(ctx context.Context, id uint64) (*model.User, error)
}

func NewService(repo repository.AuditRepository, userRepo userFinder, employeeRepo directory.EmployeeDirectory, buffer int, log *logger.Logger) *Service {
	if buffer <= 0 {
		buffer = 256
	}
	svc := &Service{
		repo:         repo,
		userRepo:     userRepo,
		employeeRepo: employeeRepo,
		queue:        make(chan *model.AuditLog, buffer),
		stop:         make(chan struct{}),
		logger:       log,
	}
	svc.start()
	return svc
}

func (s *Service) start() {
	s.wg.Add(1)
	go func() {
		defer s.wg.Done()
		for {
			select {
			case entry, ok := <-s.queue:
				if !ok {
					return
				}
				if entry != nil && s.repo != nil {
					_ = s.repo.Create(context.Background(), entry)
				}
			case <-s.stop:
				return
			}
		}
	}()
}

func (s *Service) Log(ctx context.Context, entry Entry) {
	if s.repo == nil {
		if s.logger != nil {
			s.logger.Warn("cannot log audit entry - repository is nil",
				"action", entry.Action,
				"resource_type", entry.ResourceType,
			)
		}
		return
	}
	// Use a background context with timeout to avoid issues with request context cancellation.
	// The request context is typically cancelled right after the response is sent to the client.
	// Using a background context ensures the audit log write completes regardless.
	persistCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	resolvedUserID := s.resolveUserID(persistCtx, entry.UserID)
	actorUserID := s.resolveUserID(persistCtx, entry.ActorUserID)
	if actorUserID == nil && entry.ActorUserID == nil {
		actorUserID = resolvedUserID
	}
	actorEmail := entry.ActorEmail
	if actorEmail == "" && entry.Metadata != nil {
		if val, ok := entry.Metadata["user_email"].(string); ok {
			actorEmail = val
		}
	}
	actorType := entry.ActorType
	if actorType == "" {
		if resolvedUserID != nil || actorUserID != nil || actorEmail != "" {
			actorType = "user"
		} else {
			actorType = "system"
		}
	}

	record := &model.AuditLog{
		UserID:       resolvedUserID,
		ActorType:    actorType,
		ActorUserID:  actorUserID,
		ActorEmail:   actorEmail,
		Action:       entry.Action,
		ResourceType: entry.ResourceType,
		ResourceID:   entry.ResourceID,
		Metadata:     entry.Metadata,
		BeforeState:  entry.BeforeState,
		AfterState:   entry.AfterState,
		IPAddress:    entry.IPAddress,
		UserAgent:    entry.UserAgent,
	}

	// Write synchronously to ensure immediate persistence and availability for queries.
	// This ensures that when clients fetch audit logs immediately after an action,
	// the logs are already in the database.
	if err := s.repo.Create(persistCtx, record); err != nil {
		// Log the error for debugging
		if s.logger != nil {
			s.logger.Error("failed to create audit log (sync write)",
				"error", err,
				"action", entry.Action,
				"resource_type", entry.ResourceType,
				"user_id", resolvedUserID,
			)
		}
		// If sync write fails, attempt to queue for async retry as a fallback
		select {
		case s.queue <- record:
			// Fallback to async processing
			if s.logger != nil {
				s.logger.Info("queued failed audit log for async retry",
					"action", entry.Action,
					"resource_type", entry.ResourceType,
				)
			}
		default:
			// Queue full and sync failed - log is lost
			if s.logger != nil {
				s.logger.Error("audit log queue full, entry lost (both sync and async failed)",
					"action", entry.Action,
					"resource_type", entry.ResourceType,
					"user_id", resolvedUserID,
				)
			}
		}
	}
}

func (s *Service) resolveUserID(ctx context.Context, userID *uint64) *uint64 {
	if userID == nil || *userID == 0 {
		return nil
	}
	if s.userRepo == nil {
		return userID
	}
	if cached, ok := s.userExists.Load(*userID); ok {
		if valid, _ := cached.(bool); valid {
			return userID
		}
		return nil
	}
	user, err := s.userRepo.FindByID(ctx, *userID)
	if err != nil || user == nil {
		s.userExists.Store(*userID, false)
		if s.logger != nil {
			s.logger.Warn("dropping audit user foreign key reference",
				"user_id", *userID,
				"error", err,
			)
		}
		return nil
	}
	s.userExists.Store(*userID, true)
	return userID
}

func (s *Service) List(ctx context.Context, filter repository.AuditFilter, page, pageSize int) ([]model.AuditLog, int64, error) {
	if s.repo == nil {
		return []model.AuditLog{}, 0, nil
	}
	logs, total, err := s.repo.List(ctx, filter, page, pageSize)
	if err != nil || len(logs) == 0 {
		return logs, total, err
	}
	if s.employeeRepo == nil {
		return logs, total, nil
	}

	emailSet := make(map[string]struct{})
	emailByIndex := make(map[int]string)
	for i := range logs {
		if logs[i].ActorUser != nil && logs[i].ActorUser.Employee != nil {
			logs[i].ActorEmployee = logs[i].ActorUser.Employee
		}
		email := strings.TrimSpace(logs[i].ActorEmail)
		if email == "" && logs[i].ActorUser != nil && logs[i].ActorUser.Employee != nil {
			email = strings.TrimSpace(logs[i].ActorUser.Employee.Email)
		}
		if email == "" && logs[i].Metadata != nil {
			if val, ok := logs[i].Metadata["user_email"].(string); ok {
				email = strings.TrimSpace(val)
			}
		}
		if email == "" {
			continue
		}
		normalized := strings.ToLower(email)
		emailByIndex[i] = normalized
		emailSet[normalized] = struct{}{}
	}
	if len(emailSet) == 0 {
		return logs, total, nil
	}
	emails := make([]string, 0, len(emailSet))
	for email := range emailSet {
		emails = append(emails, email)
	}

	employees, err := s.employeeRepo.FindByEmails(ctx, emails)
	if err != nil {
		if s.logger != nil {
			s.logger.Warn("failed to fetch employees for audit logs", "error", err)
		}
		return logs, total, nil
	}
	empByEmail := make(map[string]*directory.Employee, len(employees))
	for i := range employees {
		email := strings.ToLower(strings.TrimSpace(employees[i].Email))
		if email != "" {
			empByEmail[email] = &employees[i]
		}
	}
	for i := range logs {
		if email, ok := emailByIndex[i]; ok {
			if emp, exists := empByEmail[email]; exists {
				logs[i].ActorEmployee = emp
			}
		}
	}
	return logs, total, nil
}

func (s *Service) Get(ctx context.Context, id uint64) (*model.AuditLog, error) {
	if s.repo == nil {
		return nil, nil
	}
	log, err := s.repo.FindByID(ctx, id)
	if err != nil || log == nil || s.employeeRepo == nil {
		return log, err
	}
	if log.ActorUser != nil && log.ActorUser.Employee != nil {
		log.ActorEmployee = log.ActorUser.Employee
	}
	email := strings.TrimSpace(log.ActorEmail)
	if email == "" && log.ActorUser != nil && log.ActorUser.Employee != nil {
		email = strings.TrimSpace(log.ActorUser.Employee.Email)
	}
	if email == "" && log.Metadata != nil {
		if val, ok := log.Metadata["user_email"].(string); ok {
			email = strings.TrimSpace(val)
		}
	}
	if email == "" {
		return log, nil
	}
	employees, lookupErr := s.employeeRepo.FindByEmails(ctx, []string{strings.ToLower(email)})
	if lookupErr != nil || len(employees) == 0 {
		return log, nil
	}
	log.ActorEmployee = &employees[0]
	return log, nil
}

func (s *Service) Stats(ctx context.Context) (*repository.AuditStats, error) {
	if s.repo == nil {
		return &repository.AuditStats{}, nil
	}
	return s.repo.Stats(ctx)
}

func (s *Service) Shutdown(ctx context.Context) error {
	s.once.Do(func() {
		close(s.stop)
		close(s.queue)
	})
	done := make(chan struct{})
	go func() {
		s.wg.Wait()
		close(done)
	}()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-done:
		return nil
	}
}

func (s *Service) Flush(timeout time.Duration) {
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()
	_ = s.Shutdown(ctx)
}
