package processor

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"eslogad-be/internal/entities"
	"eslogad-be/internal/repository"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
	"gorm.io/gorm"
)

type OnlyOfficeProcessor interface {
	GetDocumentSessionByKey(ctx context.Context, documentKey string) (*entities.DocumentSession, error)
	GetOrCreateDocumentSession(ctx context.Context, documentID, userID uuid.UUID) (*entities.DocumentSession, error)
	UpdateDocumentSession(ctx context.Context, session *entities.DocumentSession) error
	CreateDocumentVersion(ctx context.Context, version *entities.DocumentVersion) error
	UpdateDocumentURL(ctx context.Context, documentID uuid.UUID, newURL string) error
	LockDocument(ctx context.Context, documentID, userID uuid.UUID) error
	UnlockDocument(ctx context.Context, documentID, userID uuid.UUID) error
	LogDocumentError(ctx context.Context, documentID uuid.UUID, errorMsg string, details interface{}) error
	GetDocumentDetails(ctx context.Context, documentID uuid.UUID, documentType string) (*DocumentDetails, error)
}

type DocumentDetails struct {
	DocumentID   uuid.UUID
	FileName     string
	FileType     string
	FileURL      string
	FileSize     int64
	DocumentType string
	ReferenceID  uuid.UUID
}

type OnlyOfficeProcessorImpl struct {
	db                *gorm.DB
	sessionRepo       *repository.DocumentSessionRepository
	versionRepo       *repository.DocumentVersionRepository
	metadataRepo      *repository.DocumentMetadataRepository
	errorRepo         *repository.DocumentErrorRepository
	txManager         *repository.TxManager
}

func NewOnlyOfficeProcessor(
	db *gorm.DB,
	sessionRepo *repository.DocumentSessionRepository,
	versionRepo *repository.DocumentVersionRepository,
	metadataRepo *repository.DocumentMetadataRepository,
	errorRepo *repository.DocumentErrorRepository,
	txManager *repository.TxManager,
) *OnlyOfficeProcessorImpl {
	return &OnlyOfficeProcessorImpl{
		db:           db,
		sessionRepo:  sessionRepo,
		versionRepo:  versionRepo,
		metadataRepo: metadataRepo,
		errorRepo:    errorRepo,
		txManager:    txManager,
	}
}

// GetDocumentSessionByKey retrieves a document session by its OnlyOffice key
func (p *OnlyOfficeProcessorImpl) GetDocumentSessionByKey(ctx context.Context, documentKey string) (*entities.DocumentSession, error) {
	return p.sessionRepo.GetByKey(ctx, documentKey)
}

// GetOrCreateDocumentSession gets an existing session or creates a new one
func (p *OnlyOfficeProcessorImpl) GetOrCreateDocumentSession(ctx context.Context, documentID, userID uuid.UUID) (*entities.DocumentSession, error) {
	// Try to get existing active session
	session, err := p.sessionRepo.GetActiveByDocument(ctx, documentID)
	if err == nil && session != nil {
		// Return existing session if it's not closed
		if session.Status != 4 { // Status 4 = closed
			return session, nil
		}
	}

	// Create new session
	session = &entities.DocumentSession{
		DocumentID:  documentID,
		UserID:      userID,
		Status:      0, // Initial status
		Version:     1,
		IsLocked:    false,
	}

	// Generate unique document key with retry logic
	for attempts := 0; attempts < 3; attempts++ {
		session.DocumentKey = p.generateDocumentKey(documentID, userID)
		err = p.sessionRepo.Create(ctx, session)
		if err == nil {
			return session, nil
		}
		// If it's not a duplicate key error, return the error
		if !errors.Is(err, gorm.ErrDuplicatedKey) && !contains(err.Error(), "duplicate key") {
			return nil, fmt.Errorf("failed to create document session: %w", err)
		}
		// Wait a bit before retrying
		time.Sleep(time.Millisecond * 10)
	}

	return nil, fmt.Errorf("failed to create document session after retries: %w", err)
}

// UpdateDocumentSession updates an existing document session
func (p *OnlyOfficeProcessorImpl) UpdateDocumentSession(ctx context.Context, session *entities.DocumentSession) error {
	return p.sessionRepo.Update(ctx, session)
}

// CreateDocumentVersion creates a new document version
func (p *OnlyOfficeProcessorImpl) CreateDocumentVersion(ctx context.Context, version *entities.DocumentVersion) error {
	return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
		// Deactivate previous versions if this is the active one
		if version.IsActive {
			err := p.versionRepo.DeactivateAllVersions(txCtx, version.DocumentID)
			if err != nil {
				return fmt.Errorf("failed to deactivate previous versions: %w", err)
			}
		}

		// Create new version
		err := p.versionRepo.Create(txCtx, version)
		if err != nil {
			return fmt.Errorf("failed to create document version: %w", err)
		}

		return nil
	})
}

// UpdateDocumentURL updates the document URL in the appropriate table
func (p *OnlyOfficeProcessorImpl) UpdateDocumentURL(ctx context.Context, documentID uuid.UUID, newURL string) error {
	// Get document metadata to determine type
	metadata, err := p.metadataRepo.GetByDocumentID(ctx, documentID)
	if err != nil {
		return fmt.Errorf("failed to get document metadata: %w", err)
	}

	// Update based on document type
	switch metadata.DocumentType {
	case "letter_attachment":
		return p.updateLetterAttachmentURL(ctx, metadata.ReferenceID, newURL)
	case "outgoing_attachment":
		return p.updateOutgoingAttachmentURL(ctx, metadata.ReferenceID, newURL)
	case "discussion_attachment":
		return p.updateDiscussionAttachmentURL(ctx, metadata.ReferenceID, newURL)
	default:
		return fmt.Errorf("unknown document type: %s", metadata.DocumentType)
	}
}

// LockDocument locks a document for editing
func (p *OnlyOfficeProcessorImpl) LockDocument(ctx context.Context, documentID, userID uuid.UUID) error {
	session, err := p.sessionRepo.GetActiveByDocument(ctx, documentID)
	if err != nil {
		return fmt.Errorf("failed to get document session: %w", err)
	}

	// Check if already locked by another user
	if session.IsLocked && session.LockedBy != nil && *session.LockedBy != userID {
		return errors.New("document is already locked by another user")
	}

	// Lock the document
	session.IsLocked = true
	session.LockedBy = &userID
	now := time.Now()
	session.LockedAt = &now

	return p.sessionRepo.Update(ctx, session)
}

// UnlockDocument unlocks a document
func (p *OnlyOfficeProcessorImpl) UnlockDocument(ctx context.Context, documentID, userID uuid.UUID) error {
	session, err := p.sessionRepo.GetActiveByDocument(ctx, documentID)
	if err != nil {
		return fmt.Errorf("failed to get document session: %w", err)
	}

	// Check if locked by the requesting user
	if session.IsLocked && session.LockedBy != nil && *session.LockedBy != userID {
		return errors.New("document is locked by another user")
	}

	// Unlock the document
	session.IsLocked = false
	session.LockedBy = nil
	session.LockedAt = nil

	return p.sessionRepo.Update(ctx, session)
}

// LogDocumentError logs an error related to document operations
func (p *OnlyOfficeProcessorImpl) LogDocumentError(ctx context.Context, documentID uuid.UUID, errorMsg string, details interface{}) error {
	detailsMap := make(map[string]interface{})
	
	// Convert details to map if possible
	if details != nil {
		detailsJSON, _ := json.Marshal(details)
		json.Unmarshal(detailsJSON, &detailsMap)
	}

	docError := &entities.DocumentError{
		DocumentID: documentID,
		ErrorType:  "onlyoffice_callback",
		ErrorMsg:   errorMsg,
		Details:    detailsMap,
	}

	// Get current session if available
	if session, err := p.sessionRepo.GetActiveByDocument(ctx, documentID); err == nil && session != nil {
		docError.SessionID = &session.ID
	}

	return p.errorRepo.Create(ctx, docError)
}

// GetDocumentDetails retrieves document details based on type
func (p *OnlyOfficeProcessorImpl) GetDocumentDetails(ctx context.Context, documentID uuid.UUID, documentType string) (*DocumentDetails, error) {
	// Get metadata
	metadata, err := p.metadataRepo.GetByDocumentID(ctx, documentID)
	if err != nil {
		// If metadata doesn't exist, create it based on the document type
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return p.createDocumentMetadata(ctx, documentID, documentType)
		}
		return nil, err
	}

	// Get the actual file URL from the appropriate table
	fileURL, err := p.getDocumentURL(ctx, metadata)
	if err != nil {
		return nil, err
	}

	return &DocumentDetails{
		DocumentID:   metadata.DocumentID,
		FileName:     metadata.FileName,
		FileType:     metadata.FileType,
		FileURL:      fileURL,
		FileSize:     metadata.FileSize,
		DocumentType: metadata.DocumentType,
		ReferenceID:  metadata.ReferenceID,
	}, nil
}

// Helper methods

func (p *OnlyOfficeProcessorImpl) generateDocumentKey(documentID, userID uuid.UUID) string {
	// Use nanoseconds for better precision
	now := time.Now().UnixNano()
	
	// Generate random bytes for additional uniqueness
	randomBytes := make([]byte, 4)
	rand.Read(randomBytes)
	randomHex := hex.EncodeToString(randomBytes)
	
	return fmt.Sprintf("%s_%s_%d_%s", documentID.String()[:8], userID.String()[:8], now, randomHex)
}

func (p *OnlyOfficeProcessorImpl) updateLetterAttachmentURL(ctx context.Context, attachmentID uuid.UUID, newURL string) error {
	// Update letter attachment URL
	return p.db.WithContext(ctx).
		Table("letter_incoming_attachments").
		Where("id = ?", attachmentID).
		Update("file_url", newURL).Error
}

func (p *OnlyOfficeProcessorImpl) updateOutgoingAttachmentURL(ctx context.Context, attachmentID uuid.UUID, newURL string) error {
	// Update outgoing letter attachment URL
	return p.db.WithContext(ctx).
		Table("letter_outgoing_attachments").
		Where("id = ?", attachmentID).
		Update("file_url", newURL).Error
}

func (p *OnlyOfficeProcessorImpl) updateDiscussionAttachmentURL(ctx context.Context, attachmentID uuid.UUID, newURL string) error {
	// Update discussion attachment URL
	return p.db.WithContext(ctx).
		Table("letter_outgoing_discussion_attachments").
		Where("id = ?", attachmentID).
		Update("file_url", newURL).Error
}

func (p *OnlyOfficeProcessorImpl) createDocumentMetadata(ctx context.Context, documentID uuid.UUID, documentType string) (*DocumentDetails, error) {
	var metadata entities.DocumentMetadata
	var details DocumentDetails
	
	// Fetch document information based on type
	switch documentType {
	case "letter_attachment":
		var attachment struct {
			ID       uuid.UUID `gorm:"column:id"`
			LetterID uuid.UUID `gorm:"column:letter_id"`
			FileName string    `gorm:"column:file_name"`
			FileURL  string    `gorm:"column:file_url"`
			FileSize int64     `gorm:"column:file_size"`
			FileType string    `gorm:"column:file_type"`
		}
		err := p.db.WithContext(ctx).
			Table("letter_incoming_attachments").
			Where("id = ?", documentID).
			First(&attachment).Error
		if err != nil {
			return nil, fmt.Errorf("failed to get letter attachment: %w", err)
		}
		
		metadata = entities.DocumentMetadata{
			DocumentID:   documentID,
			DocumentType: documentType,
			ReferenceID:  attachment.LetterID,
			FileName:     attachment.FileName,
			FileType:     attachment.FileType,
			FileSize:     attachment.FileSize,
		}
		details = DocumentDetails{
			DocumentID:   documentID,
			FileName:     attachment.FileName,
			FileType:     attachment.FileType,
			FileURL:      attachment.FileURL,
			FileSize:     attachment.FileSize,
			DocumentType: documentType,
			ReferenceID:  attachment.LetterID,
		}
		
	case "outgoing_attachment":
		var attachment struct {
			ID       uuid.UUID `gorm:"column:id"`
			LetterID uuid.UUID `gorm:"column:letter_id"`
			FileName string    `gorm:"column:file_name"`
			FileURL  string    `gorm:"column:file_url"`
			FileSize int64     `gorm:"column:file_size"`
			FileType string    `gorm:"column:file_type"`
		}
		err := p.db.WithContext(ctx).
			Table("letter_outgoing_attachments").
			Where("id = ?", documentID).
			First(&attachment).Error
		if err != nil {
			return nil, fmt.Errorf("failed to get outgoing letter attachment: %w", err)
		}
		
		metadata = entities.DocumentMetadata{
			DocumentID:   documentID,
			DocumentType: documentType,
			ReferenceID:  attachment.LetterID,
			FileName:     attachment.FileName,
			FileType:     attachment.FileType,
			FileSize:     attachment.FileSize,
		}
		details = DocumentDetails{
			DocumentID:   documentID,
			FileName:     attachment.FileName,
			FileType:     attachment.FileType,
			FileURL:      attachment.FileURL,
			FileSize:     attachment.FileSize,
			DocumentType: documentType,
			ReferenceID:  attachment.LetterID,
		}
		
	case "discussion_attachment":
		var attachment struct {
			ID           uuid.UUID `gorm:"column:id"`
			DiscussionID uuid.UUID `gorm:"column:discussion_id"`
			FileName     string    `gorm:"column:file_name"`
			FileURL      string    `gorm:"column:file_url"`
			FileSize     int64     `gorm:"column:file_size"`
			FileType     string    `gorm:"column:file_type"`
		}
		err := p.db.WithContext(ctx).
			Table("letter_outgoing_discussion_attachments").
			Where("id = ?", documentID).
			First(&attachment).Error
		if err != nil {
			return nil, fmt.Errorf("failed to get discussion attachment: %w", err)
		}
		
		metadata = entities.DocumentMetadata{
			DocumentID:   documentID,
			DocumentType: documentType,
			ReferenceID:  attachment.DiscussionID,
			FileName:     attachment.FileName,
			FileType:     attachment.FileType,
			FileSize:     attachment.FileSize,
		}
		details = DocumentDetails{
			DocumentID:   documentID,
			FileName:     attachment.FileName,
			FileType:     attachment.FileType,
			FileURL:      attachment.FileURL,
			FileSize:     attachment.FileSize,
			DocumentType: documentType,
			ReferenceID:  attachment.DiscussionID,
		}
		
	default:
		return nil, fmt.Errorf("unknown document type: %s", documentType)
	}
	
	// Create metadata in database
	err := p.metadataRepo.Create(ctx, &metadata)
	if err != nil {
		// If it already exists (race condition), try to get it
		if strings.Contains(err.Error(), "duplicate key") {
			existingMetadata, getErr := p.metadataRepo.GetByDocumentID(ctx, documentID)
			if getErr == nil {
				// Get the file URL
				fileURL, urlErr := p.getDocumentURL(ctx, existingMetadata)
				if urlErr == nil {
					details.FileURL = fileURL
				}
				return &details, nil
			}
		}
		return nil, fmt.Errorf("failed to create document metadata: %w", err)
	}
	
	return &details, nil
}

func (p *OnlyOfficeProcessorImpl) getDocumentURL(ctx context.Context, metadata *entities.DocumentMetadata) (string, error) {
	var fileURL string
	
	switch metadata.DocumentType {
	case "letter_attachment":
		err := p.db.WithContext(ctx).
			Table("letter_incoming_attachments").
			Where("id = ?", metadata.ReferenceID).
			Select("file_url").
			Scan(&fileURL).Error
		return fileURL, err
		
	case "outgoing_attachment":
		err := p.db.WithContext(ctx).
			Table("letter_outgoing_attachments").
			Where("id = ?", metadata.ReferenceID).
			Select("file_url").
			Scan(&fileURL).Error
		return fileURL, err
		
	case "discussion_attachment":
		err := p.db.WithContext(ctx).
			Table("letter_outgoing_discussion_attachments").
			Where("id = ?", metadata.ReferenceID).
			Select("file_url").
			Scan(&fileURL).Error
		return fileURL, err
		
	default:
		return "", fmt.Errorf("unknown document type: %s", metadata.DocumentType)
	}
}

// Helper function to check if string contains substring
func contains(s, substr string) bool {
	return strings.Contains(s, substr)
}