package processor

import (
	"context"
	"fmt"
	"time"

	"eslogad-be/internal/appcontext"
	"eslogad-be/internal/contract"
	"eslogad-be/internal/entities"
	"eslogad-be/internal/repository"
	"eslogad-be/internal/transformer"

	"github.com/google/uuid"
)

type LetterProcessorImpl struct {
	letterRepo               *repository.LetterIncomingRepository
	attachRepo               *repository.LetterIncomingAttachmentRepository
	txManager                *repository.TxManager
	activity                 *ActivityLogProcessorImpl
	dispositionRepo          *repository.LetterIncomingDispositionRepository
	dispositionDeptRepo      *repository.LetterIncomingDispositionDepartmentRepository
	dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
	dispositionNoteRepo      *repository.DispositionNoteRepository
	discussionRepo           *repository.LetterDiscussionRepository
	settingRepo              *repository.AppSettingRepository
	recipientRepo            *repository.LetterIncomingRecipientRepository
	outgoingRecipientRepo    *repository.LetterOutgoingRecipientRepository
	departmentRepo           *repository.DepartmentRepository
	userDeptRepo             *repository.UserDepartmentRepository
	priorityRepo             *repository.PriorityRepository
	institutionRepo          *repository.InstitutionRepository
	dispActionRepo           *repository.DispositionActionRepository
	dispoRoutes              *repository.DispositionRouteRepository
	numberGenerator          *LetterNumberGeneratorImpl
}

func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterIncomingDispositionRepository, dispDeptRepo *repository.LetterIncomingDispositionDepartmentRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository,
	settingRepo *repository.AppSettingRepository,
	recipientRepo *repository.LetterIncomingRecipientRepository,
	outgoingRecipientRepo *repository.LetterOutgoingRecipientRepository,
	departmentRepo *repository.DepartmentRepository,
	userDeptRepo *repository.UserDepartmentRepository,
	priorityRepo *repository.PriorityRepository,
	institutionRepo *repository.InstitutionRepository,
	dispActionRepo *repository.DispositionActionRepository,
	numberGenerator *LetterNumberGeneratorImpl,
	dispoRoutes *repository.DispositionRouteRepository) *LetterProcessorImpl {
	return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager,
		activity: activity, dispositionRepo: dispRepo, dispositionDeptRepo: dispDeptRepo,
		dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo,
		discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo,
		outgoingRecipientRepo: outgoingRecipientRepo,
		departmentRepo:        departmentRepo, userDeptRepo: userDeptRepo, priorityRepo: priorityRepo,
		institutionRepo: institutionRepo, dispActionRepo: dispActionRepo, numberGenerator: numberGenerator,
		dispoRoutes: dispoRoutes}
}

func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
	userID := appcontext.FromGinContext(ctx).UserID

	entity := &entities.LetterIncoming{
		LetterNumber:        req.LetterNumber,
		ReferenceNumber:     req.ReferenceNumber,
		Subject:             req.Subject,
		Description:         req.Description,
		PriorityID:          req.PriorityID,
		SenderInstitutionID: req.SenderInstitutionID,
		ReceivedDate:        req.ReceivedDate,
		DueDate:             req.DueDate,
		Status:              entities.LetterIncomingStatusNew,
		CreatedBy:           userID,
	}

	if err := p.letterRepo.Create(ctx, entity); err != nil {
		return nil, err
	}

	if err := p.createAttachments(ctx, entity.ID, req.Attachments, userID); err != nil {
		return nil, err
	}

	return p.buildLetterResponse(ctx, entity)
}

func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
	// Get current user ID from context
	userID := appcontext.FromGinContext(ctx).UserID

	entity, err := p.letterRepo.Get(ctx, id)
	if err != nil {
		return nil, err
	}
	atts, _ := p.attachRepo.ListByLetter(ctx, id)
	var pr *entities.Priority
	if entity.PriorityID != nil && p.priorityRepo != nil {
		if got, err := p.priorityRepo.Get(ctx, *entity.PriorityID); err == nil {
			pr = got
		}
	}
	var inst *entities.Institution
	if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
		if got, err := p.institutionRepo.Get(ctx, *entity.SenderInstitutionID); err == nil {
			inst = got
		}
	}

	// Check if letter is read by current user
	isRead := false
	if p.recipientRepo != nil {
		if recipient, err := p.recipientRepo.GetByLetterAndUser(ctx, id, userID); err == nil {
			isRead = recipient.ReadAt != nil
		}
	}

	resp := transformer.LetterEntityToContract(entity, atts, pr, inst)
	resp.IsRead = isRead

	// Include created_by if the current user is the creator
	if entity.CreatedBy == userID {
		resp.CreatedBy = entity.CreatedBy
	}

	return resp, nil
}

func (p *LetterProcessorImpl) GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error) {
	userID := appcontext.FromGinContext(ctx).UserID

	incomingUnread := 0
	if p.recipientRepo != nil {
		if count, err := p.recipientRepo.CountUnreadByUser(ctx, userID); err == nil {
			incomingUnread = count
		}
	}

	outgoingUnread := 0
	if p.outgoingRecipientRepo != nil {
		if count, err := p.outgoingRecipientRepo.CountUnreadByUser(ctx, userID); err == nil {
			outgoingUnread = count
		}
	}

	response := &contract.LetterUnreadCountResponse{}
	response.IncomingLetter.Unread = incomingUnread
	response.OutgoingLetter.Unread = outgoingUnread

	return response, nil
}

func (p *LetterProcessorImpl) MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
	// Get current user ID from context
	userID := appcontext.FromGinContext(ctx).UserID

	// Mark the letter as read for the current user
	if p.recipientRepo != nil {
		if err := p.recipientRepo.MarkAsRead(ctx, letterID, userID); err != nil {
			return &contract.MarkLetterReadResponse{
				Success: false,
				Message: "Failed to mark letter as read",
			}, err
		}
	}

	return &contract.MarkLetterReadResponse{
		Success: true,
		Message: "Letter marked as read successfully",
	}, nil
}

func (p *LetterProcessorImpl) MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
	// Get current user ID from context
	userID := appcontext.FromGinContext(ctx).UserID

	// Mark the letter as read for the current user
	if p.outgoingRecipientRepo != nil {
		if err := p.outgoingRecipientRepo.MarkAsRead(ctx, letterID, userID); err != nil {
			return &contract.MarkLetterReadResponse{
				Success: false,
				Message: "Failed to mark letter as read",
			}, err
		}
	}

	return &contract.MarkLetterReadResponse{
		Success: true,
		Message: "Letter marked as read successfully",
	}, nil
}

func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, filter repository.ListIncomingLettersFilter, page, limit int) ([]entities.LetterIncoming, int64, error) {
	// Just fetch the raw data
	return p.letterRepo.List(ctx, filter, limit, (page-1)*limit)
}

func (p *LetterProcessorImpl) GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error) {
	if p.attachRepo == nil || len(letterIDs) == 0 {
		return make(map[uuid.UUID][]entities.LetterIncomingAttachment), nil
	}
	return p.attachRepo.ListByLetterIDs(ctx, letterIDs)
}

func (p *LetterProcessorImpl) GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error) {
	if p.priorityRepo == nil || len(priorityIDs) == 0 {
		return make(map[uuid.UUID]*entities.Priority), nil
	}
	return p.priorityRepo.GetByIDs(ctx, priorityIDs)
}

func (p *LetterProcessorImpl) GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error) {
	if p.institutionRepo == nil || len(institutionIDs) == 0 {
		return make(map[uuid.UUID]*entities.Institution), nil
	}
	return p.institutionRepo.GetByIDs(ctx, institutionIDs)
}

func (p *LetterProcessorImpl) GetBatchRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error) {
	if p.recipientRepo == nil || len(letterIDs) == 0 {
		return make(map[uuid.UUID]*entities.LetterIncomingRecipient), nil
	}
	return p.recipientRepo.GetByLetterIDsAndUser(ctx, letterIDs, userID)
}

func (p *LetterProcessorImpl) CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error) {
	if p.recipientRepo == nil {
		return 0, nil
	}
	return p.recipientRepo.CountUnreadByUser(ctx, userID)
}

func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
	var out *contract.IncomingLetterResponse
	err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
		entity, err := p.letterRepo.Get(txCtx, id)
		if err != nil {
			return err
		}
		fromStatus := string(entity.Status)
		if req.ReferenceNumber != nil {
			entity.ReferenceNumber = req.ReferenceNumber
		}
		if req.Subject != nil {
			entity.Subject = *req.Subject
		}
		if req.Description != nil {
			entity.Description = req.Description
		}
		if req.PriorityID != nil {
			entity.PriorityID = req.PriorityID
		}
		if req.SenderInstitutionID != nil {
			entity.SenderInstitutionID = req.SenderInstitutionID
		}
		if req.ReceivedDate != nil {
			entity.ReceivedDate = *req.ReceivedDate
		}
		if req.DueDate != nil {
			entity.DueDate = req.DueDate
		}
		if req.Status != nil {
			entity.Status = entities.LetterIncomingStatus(*req.Status)
		}
		if err := p.letterRepo.Update(txCtx, entity); err != nil {
			return err
		}
		toStatus := string(entity.Status)
		if p.activity != nil && fromStatus != toStatus {
			userID := appcontext.FromGinContext(txCtx).UserID
			action := "status.changed"
			if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, &fromStatus, &toStatus, map[string]interface{}{}); err != nil {
				return err
			}
		}
		atts, _ := p.attachRepo.ListByLetter(txCtx, id)
		var pr *entities.Priority
		if entity.PriorityID != nil && p.priorityRepo != nil {
			if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
				pr = got
			}
		}
		var inst *entities.Institution
		if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
			if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
				inst = got
			}
		}
		out = transformer.LetterEntityToContract(entity, atts, pr, inst)
		return nil
	})
	if err != nil {
		return nil, err
	}
	return out, nil
}

func (p *LetterProcessorImpl) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error {
	return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
		if err := p.letterRepo.SoftDelete(txCtx, id); err != nil {
			return err
		}
		if p.activity != nil {
			userID := appcontext.FromGinContext(txCtx).UserID
			action := "letter.deleted"
			if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{}); err != nil {
				return err
			}
		}
		return nil
	})
}

// CreateDispositions creates a new disposition with modular helper functions
func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
	// Transaction should be handled at service layer
	// The context passed here should already contain the transaction if needed

	// Step 1: Update existing disposition departments
	if err := p.updateExistingDispositionDepartments(ctx, req.LetterID, req.FromDepartment); err != nil {
		return nil, err
	}

	// Step 2: Create the main disposition
	disp, err := p.createMainDisposition(ctx, req)
	if err != nil {
		return nil, err
	}

	// Step 3: Create disposition departments for target departments
	dispDepartments, err := p.createDispositionDepartments(ctx, disp.ID, req.LetterID, req.ToDepartmentIDs)
	if err != nil {
		return nil, err
	}

	// Step 4: Create action selections if provided
	if err := p.createActionSelections(ctx, disp.ID, req.SelectedActions, req.CreatedBy); err != nil {
		return nil, err
	}

	// Step 5: Build and return the response
	return p.buildDispositionResponse(disp, dispDepartments, req.ToDepartmentIDs), nil
}

// updateExistingDispositionDepartments updates the status of existing disposition departments
func (p *LetterProcessorImpl) updateExistingDispositionDepartments(ctx context.Context, letterID uuid.UUID, fromDepartment uuid.UUID) error {
	existingDispDepts, err := p.dispositionDeptRepo.GetByLetterAndDepartment(ctx, letterID, fromDepartment)
	if err != nil {
		// If no existing departments found, that's ok
		return nil
	}

	for _, existingDispDept := range existingDispDepts {
		if existingDispDept.Status == entities.DispositionDepartmentStatusPending {
			existingDispDept.Status = entities.DispositionDepartmentStatusDispositioned
			if err := p.dispositionDeptRepo.Update(ctx, &existingDispDept); err != nil {
				return fmt.Errorf("failed to update existing disposition department: %w", err)
			}
		}
	}

	return nil
}

// createMainDisposition creates the primary disposition record
func (p *LetterProcessorImpl) createMainDisposition(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*entities.LetterIncomingDisposition, error) {
	disp := &entities.LetterIncomingDisposition{
		LetterID:     req.LetterID,
		DepartmentID: &req.FromDepartment,
		Notes:        req.Notes,
		CreatedBy:    req.CreatedBy, // Should be set by service layer
	}

	if err := p.dispositionRepo.Create(ctx, disp); err != nil {
		return nil, fmt.Errorf("failed to create disposition: %w", err)
	}

	return disp, nil
}

// createDispositionDepartments creates disposition department records for target departments
func (p *LetterProcessorImpl) createDispositionDepartments(ctx context.Context, dispositionID, letterID uuid.UUID, toDepartmentIDs []uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
	if len(toDepartmentIDs) == 0 {
		return nil, nil
	}

	dispDepartments := make([]entities.LetterIncomingDispositionDepartment, 0, len(toDepartmentIDs))
	for _, toDept := range toDepartmentIDs {
		dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
			LetterIncomingDispositionID: dispositionID,
			LetterIncomingID:            letterID,
			DepartmentID:                toDept,
			Status:                      entities.DispositionDepartmentStatusPending,
		})
	}

	if err := p.dispositionDeptRepo.CreateBulk(ctx, dispDepartments); err != nil {
		return nil, fmt.Errorf("failed to create disposition departments: %w", err)
	}

	return dispDepartments, nil
}

// createActionSelections creates action selection records for the disposition
func (p *LetterProcessorImpl) createActionSelections(ctx context.Context, dispositionID uuid.UUID, selectedActions []contract.CreateDispositionActionSelection, createdBy uuid.UUID) error {
	if len(selectedActions) == 0 {
		return nil
	}

	selections := make([]entities.LetterDispositionActionSelection, 0, len(selectedActions))
	for _, sel := range selectedActions {
		selections = append(selections, entities.LetterDispositionActionSelection{
			DispositionID: dispositionID,
			ActionID:      sel.ActionID,
			Note:          sel.Note,
			CreatedBy:     createdBy,
		})
	}

	if err := p.dispositionActionSelRepo.CreateBulk(ctx, selections); err != nil {
		return fmt.Errorf("failed to create action selections: %w", err)
	}

	return nil
}

// buildDispositionResponse builds the response for the created disposition
func (p *LetterProcessorImpl) buildDispositionResponse(disp *entities.LetterIncomingDisposition, dispDepartments []entities.LetterIncomingDispositionDepartment, toDepartmentIDs []uuid.UUID) *contract.ListDispositionsResponse {
	response := &contract.ListDispositionsResponse{
		Dispositions: []contract.DispositionResponse{transformer.DispoToContract(*disp)},
	}

	// The toDepartmentIDs are available in the dispDepartments for service layer logging
	// No need to store them in the response as DispositionResponse doesn't have this field

	return response
}

func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
	list, err := p.dispositionRepo.ListByLetter(ctx, letterID)
	if err != nil {
		return nil, err
	}
	return &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(list)}, nil
}

func (p *LetterProcessorImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
	// Get dispositions with all related data preloaded in a single query
	dispositions, err := p.dispositionRepo.ListByLetter(ctx, letterID)
	if err != nil {
		return nil, err
	}

	// Get discussions with preloaded user profiles
	discussions, err := p.discussionRepo.ListByLetter(ctx, letterID)
	if err != nil {
		return nil, err
	}

	// Extract all mentioned user IDs from discussions for efficient batch fetching
	var mentionedUserIDs []uuid.UUID
	mentionedUserIDsMap := make(map[uuid.UUID]bool)

	for _, discussion := range discussions {
		if discussion.Mentions != nil {
			mentions := map[string]interface{}(discussion.Mentions)
			if userIDs, ok := mentions["user_ids"]; ok {
				if userIDList, ok := userIDs.([]interface{}); ok {
					for _, userID := range userIDList {
						if userIDStr, ok := userID.(string); ok {
							if userUUID, err := uuid.Parse(userIDStr); err == nil {
								if !mentionedUserIDsMap[userUUID] {
									mentionedUserIDsMap[userUUID] = true
									mentionedUserIDs = append(mentionedUserIDs, userUUID)
								}
							}
						}
					}
				}
			}
		}
	}

	// Fetch all mentioned users in a single batch query
	var mentionedUsers []entities.User
	if len(mentionedUserIDs) > 0 {
		mentionedUsers, err = p.discussionRepo.GetUsersByIDs(ctx, mentionedUserIDs)
		if err != nil {
			return nil, err
		}
	}

	// Transform dispositions
	enhancedDispositions := transformer.EnhancedDispositionsWithPreloadedDataToContract(dispositions)

	// Transform discussions with mentioned users
	enhancedDiscussions := transformer.DiscussionsWithPreloadedDataToContract(discussions, mentionedUsers)

	return &contract.ListEnhancedDispositionsResponse{
		Dispositions: enhancedDispositions,
		Discussions:  enhancedDiscussions,
	}, nil
}

func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
	userID := appcontext.FromGinContext(ctx).UserID

	mentions := entities.JSONB(nil)
	if req.Mentions != nil {
		mentions = entities.JSONB(req.Mentions)
	}

	disc := &entities.LetterDiscussion{
		ID:       uuid.New(),
		LetterID: letterID,
		ParentID: req.ParentID,
		UserID:   userID,
		Message:  req.Message,
		Mentions: mentions,
	}

	if err := p.discussionRepo.Create(ctx, disc); err != nil {
		return nil, fmt.Errorf("failed to create discussion: %w", err)
	}

	// Activity logging should be handled at service layer
	return transformer.DiscussionEntityToContract(disc), nil
}

func (p *LetterProcessorImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, string, error) {
	// Transaction should be handled at service layer
	disc, err := p.discussionRepo.Get(ctx, discussionID)
	if err != nil {
		return nil, "", fmt.Errorf("failed to get discussion: %w", err)
	}

	// Store old message for activity logging
	oldMessage := disc.Message

	// Update discussion fields
	disc.Message = req.Message
	if req.Mentions != nil {
		disc.Mentions = entities.JSONB(req.Mentions)
	}
	now := time.Now()
	disc.EditedAt = &now

	if err := p.discussionRepo.Update(ctx, disc); err != nil {
		return nil, "", fmt.Errorf("failed to update discussion: %w", err)
	}

	// Return both the updated discussion and old message for service layer logging
	return transformer.DiscussionEntityToContract(disc), oldMessage, nil
}

func (p *LetterProcessorImpl) createAttachments(ctx context.Context, letterID uuid.UUID, attachments []contract.CreateIncomingLetterAttachment, userID uuid.UUID) error {
	if len(attachments) == 0 {
		return nil
	}

	attachmentEntities := make([]entities.LetterIncomingAttachment, 0, len(attachments))
	for _, a := range attachments {
		attachmentEntities = append(attachmentEntities, entities.LetterIncomingAttachment{
			LetterID:   letterID,
			FileURL:    a.FileURL,
			FileName:   a.FileName,
			FileType:   a.FileType,
			UploadedBy: &userID,
		})
	}

	if err := p.attachRepo.CreateBulk(ctx, attachmentEntities); err != nil {
		return err
	}

	// Attachment logging will be handled by service layer

	return nil
}

func (p *LetterProcessorImpl) buildLetterResponse(ctx context.Context, entity *entities.LetterIncoming) (*contract.IncomingLetterResponse, error) {
	savedAttachments, _ := p.attachRepo.ListByLetter(ctx, entity.ID)

	var pr *entities.Priority
	if entity.PriorityID != nil && p.priorityRepo != nil {
		if got, err := p.priorityRepo.Get(ctx, *entity.PriorityID); err == nil {
			pr = got
		}
	}

	var inst *entities.Institution
	if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
		if got, err := p.institutionRepo.Get(ctx, *entity.SenderInstitutionID); err == nil {
			inst = got
		}
	}

	return transformer.LetterEntityToContract(entity, savedAttachments, pr, inst), nil
}

func (p *LetterProcessorImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
	return p.letterRepo.BulkArchive(ctx, letterIDs)
}

// BulkArchiveIncomingLettersForUser archives letters for a specific user only
func (p *LetterProcessorImpl) BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
	return p.letterRepo.BulkArchiveForUser(ctx, letterIDs, userID)
}
