package attachmentresolver

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/shared/pkg/uuidv7"
)

type ErrorCode string

const (
	ErrorInvalidAttachmentUUID     ErrorCode = "invalid_attachment_uuid"
	ErrorInvalidFileUUID           ErrorCode = "invalid_file_uuid"
	ErrorAttachmentNotFound        ErrorCode = "attachment_not_found"
	ErrorRequireValue              ErrorCode = "require_value"
	ErrorFileManagerUnavailable    ErrorCode = "file_manager_unavailable"
	ErrorFileNotReadyForAttachment ErrorCode = "file_not_ready_for_attachment"
	ErrorCannotResolveFromFile     ErrorCode = "cannot_resolve_from_file"
)

const (
	notReadyRetryAttempts = 2
	notReadyRetryDelay    = 150 * time.Millisecond
)

type ResolveError struct {
	Code ErrorCode

	AttachmentField string
	FileField       string

	Cause error
}

func (e *ResolveError) Error() string {
	if e == nil {
		return ""
	}
	switch e.Code {
	case ErrorInvalidAttachmentUUID:
		return fmt.Sprintf("%s is invalid UUID", fallbackField(e.AttachmentField, "attachment_id"))
	case ErrorInvalidFileUUID:
		return fmt.Sprintf("%s is invalid UUID", fallbackField(e.FileField, "file_id"))
	case ErrorAttachmentNotFound:
		return fmt.Sprintf("%s not found", fallbackField(e.AttachmentField, "attachment_id"))
	case ErrorRequireValue:
		return fmt.Sprintf(
			"%s or %s is required",
			fallbackField(e.AttachmentField, "attachment_id"),
			fallbackField(e.FileField, "file_id"),
		)
	case ErrorFileManagerUnavailable:
		return "file manager service is not configured"
	case ErrorFileNotReadyForAttachment:
		return "file is not ready for attachment"
	case ErrorCannotResolveFromFile:
		return fmt.Sprintf("cannot resolve attachment from %s", fallbackField(e.FileField, "file_id"))
	default:
		if e.Cause != nil {
			return e.Cause.Error()
		}
		return "attachment resolve failed"
	}
}

func (e *ResolveError) Unwrap() error { return e.Cause }

type Params struct {
	FileManager filemanager.Service

	RawAttachmentID *string
	RawFileID       *string

	AttachmentField string
	FileField       string

	RequireValue bool

	RefType  string
	RefID    string
	Category *string

	IsPrimary bool
	ActorID   []byte

	ValidateAttachmentExists bool
	AttachmentExists         func(ctx context.Context, attachmentID []byte) (bool, error)
}

func Resolve(ctx context.Context, p Params) ([]byte, error) {
	if p.RawAttachmentID != nil && strings.TrimSpace(*p.RawAttachmentID) != "" {
		attachmentID, parseErr := uuidv7.ParseString(strings.TrimSpace(*p.RawAttachmentID))
		if parseErr != nil {
			return nil, &ResolveError{
				Code:            ErrorInvalidAttachmentUUID,
				AttachmentField: p.AttachmentField,
				FileField:       p.FileField,
				Cause:           parseErr,
			}
		}

		if p.ValidateAttachmentExists {
			exists, existsErr := resolveAttachmentExists(ctx, p, attachmentID)
			if existsErr != nil {
				return nil, existsErr
			}
			if !exists {
				return nil, &ResolveError{
					Code:            ErrorAttachmentNotFound,
					AttachmentField: p.AttachmentField,
					FileField:       p.FileField,
				}
			}
		}
		return attachmentID, nil
	}

	if p.RawFileID != nil && strings.TrimSpace(*p.RawFileID) != "" {
		if p.FileManager == nil {
			return nil, &ResolveError{
				Code:            ErrorFileManagerUnavailable,
				AttachmentField: p.AttachmentField,
				FileField:       p.FileField,
			}
		}

		fileID, parseErr := uuidv7.ParseString(strings.TrimSpace(*p.RawFileID))
		if parseErr != nil {
			return nil, &ResolveError{
				Code:            ErrorInvalidFileUUID,
				AttachmentField: p.AttachmentField,
				FileField:       p.FileField,
				Cause:           parseErr,
			}
		}

		created, createErr := p.FileManager.CreateAttachment(ctx, filemanager.CreateAttachmentParams{
			FileID:    fileID,
			RefType:   p.RefType,
			RefID:     p.RefID,
			Category:  p.Category,
			SortOrder: 0,
			IsPrimary: p.IsPrimary,
			ActorID:   p.ActorID,
		})
		if createErr == nil && created != nil && len(created.ID) == 16 {
			return created.ID, nil
		}
		if createErr != nil &&
			!errors.Is(createErr, filemanager.ErrAttachmentAlreadyExists) &&
			!errors.Is(createErr, filemanager.ErrFileNotReadyForAttachment) {
			return nil, createErr
		}
		if failed, failureErr := fileIsFailed(ctx, p.FileManager, fileID); failureErr != nil {
			return nil, failureErr
		} else if failed {
			return nil, &ResolveError{
				Code:            ErrorCannotResolveFromFile,
				AttachmentField: p.AttachmentField,
				FileField:       p.FileField,
			}
		}
		if errors.Is(createErr, filemanager.ErrFileNotReadyForAttachment) {
			return nil, &ResolveError{
				Code:            ErrorFileNotReadyForAttachment,
				AttachmentField: p.AttachmentField,
				FileField:       p.FileField,
			}
		}

		rows, _, listErr := p.FileManager.ListAttachmentsByReference(ctx, filemanager.ListAttachmentsParams{
			RefType: p.RefType,
			RefID:   p.RefID,
			Limit:   100,
			Offset:  0,
		})
		if listErr != nil {
			return nil, listErr
		}
		for i := range rows {
			if string(rows[i].FileID) == string(fileID) {
				return rows[i].ID, nil
			}
		}
		return nil, &ResolveError{
			Code:            ErrorCannotResolveFromFile,
			AttachmentField: p.AttachmentField,
			FileField:       p.FileField,
		}
	}

	if p.RequireValue {
		return nil, &ResolveError{
			Code:            ErrorRequireValue,
			AttachmentField: p.AttachmentField,
			FileField:       p.FileField,
		}
	}
	return nil, nil
}

func resolveAttachmentExists(ctx context.Context, p Params, attachmentID []byte) (bool, error) {
	if p.AttachmentExists != nil {
		return p.AttachmentExists(ctx, attachmentID)
	}
	if p.FileManager != nil {
		row, err := p.FileManager.GetAttachmentByID(ctx, attachmentID)
		if err != nil {
			return false, err
		}
		return row != nil, nil
	}
	return true, nil
}

func fileIsFailed(ctx context.Context, fm filemanager.Service, fileID []byte) (bool, error) {
	if fm == nil || len(fileID) == 0 {
		return false, nil
	}
	file, err := fm.GetFileByID(ctx, fileID)
	if err != nil {
		return false, err
	}
	if file == nil {
		return false, nil
	}
	return filemanager.NormalizeFileStatus(file.Status) == filemanager.FileStatusFailed, nil
}

func fallbackField(v string, fallback string) string {
	if strings.TrimSpace(v) == "" {
		return fallback
	}
	return strings.TrimSpace(v)
}

func sleepWithContext(ctx context.Context, d time.Duration) error {
	timer := time.NewTimer(d)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}
