package download

import (
	"context"
	"errors"
	"slices"
	"strings"
	"time"

	"github.com/google/uuid"
)

type Status string

const (
	StatusQueued      Status = "queued"
	StatusPreparing   Status = "preparing"
	StatusDownloading Status = "downloading"
	StatusStalled     Status = "stalled"
	StatusRetrying    Status = "retrying"
	StatusCompleted   Status = "completed"
	StatusFailed      Status = "failed"
	StatusCancelled   Status = "cancelled"
)

var (
	ErrInvalidInput = errors.New("invalid input")
	ErrNotFound     = errors.New("download job not found")
)

type Job struct {
	ID                string    `json:"id"`
	UserID            string    `json:"userId,omitempty"`
	SourceType        string    `json:"sourceType"`
	Source            string    `json:"source,omitempty"`
	Title             string    `json:"title"`
	Status            string    `json:"status"`
	QueuePosition     int       `json:"queuePosition,omitempty"`
	ProgressPercent   int       `json:"progressPercent"`
	BytesTotal        int64     `json:"bytesTotal"`
	BytesDownloaded   int64     `json:"bytesDownloaded"`
	DownloadSpeedMbps float64   `json:"downloadSpeedMbps"`
	EtaSeconds        int       `json:"etaSeconds,omitempty"`
	ErrorMessage      string    `json:"errorMessage,omitempty"`
	RetryCount        int       `json:"retryCount"`
	CreatedAt         time.Time `json:"createdAt,omitempty"`
	UpdatedAt         time.Time `json:"updatedAt,omitempty"`
	StartedAt         time.Time `json:"startedAt,omitempty"`
	CompletedAt       time.Time `json:"completedAt,omitempty"`
	CancelledAt       time.Time `json:"cancelledAt,omitempty"`
}

type Event struct {
	ID              int64     `json:"id"`
	JobID           string    `json:"jobId"`
	Status          string    `json:"status"`
	Message         string    `json:"message"`
	ProgressPercent int       `json:"progressPercent"`
	Payload         string    `json:"payload"`
	CreatedAt       time.Time `json:"createdAt"`
}

type CreateInput struct {
	SourceType string
	Source     string
	Title      string
}

type ListParams struct {
	Status string
	Limit  int
	Offset int
}

type EventParams struct {
	Limit int
	After string
}

type UpdateInput struct {
	Status            string
	ProgressPercent   int
	BytesTotal        int64
	BytesDownloaded   int64
	DownloadSpeedMbps float64
	EtaSeconds        int
	ErrorMessage      string
	RetryCount        int
}

type Repository interface {
	CreateJob(ctx context.Context, userID uuid.UUID, input CreateInput) (Job, error)
	ListJobs(ctx context.Context, userID uuid.UUID, params ListParams) ([]Job, error)
	GetJobByID(ctx context.Context, userID uuid.UUID, jobID string) (Job, error)
	CancelJob(ctx context.Context, userID uuid.UUID, jobID string) (Job, error)
	ListEvents(ctx context.Context, userID uuid.UUID, jobID string, params EventParams) ([]Event, error)
	AppendEvent(ctx context.Context, jobID string, event Event) error
	UpdateJob(ctx context.Context, userID uuid.UUID, jobID string, input UpdateInput) (Job, error)
}

type Service struct {
	repo Repository
}

func NewService(repo Repository) *Service {
	return &Service{repo: repo}
}

func (s *Service) Create(ctx context.Context, userID uuid.UUID, input CreateInput) (Job, error) {
	if userID == uuid.Nil {
		return Job{}, ErrInvalidInput
	}

	sourceType := strings.TrimSpace(strings.ToLower(input.SourceType))
	source := strings.TrimSpace(input.Source)
	title := strings.TrimSpace(input.Title)

	if sourceType == "" || source == "" {
		return Job{}, ErrInvalidInput
	}

	if !isAllowedSourceType(sourceType) {
		return Job{}, ErrInvalidInput
	}

	return s.repo.CreateJob(ctx, userID, CreateInput{
		SourceType: sourceType,
		Source:     source,
		Title:      title,
	})
}

func (s *Service) List(ctx context.Context, userID uuid.UUID, params ListParams) ([]Job, error) {
	if userID == uuid.Nil {
		return nil, ErrInvalidInput
	}

	limit := params.Limit
	if limit < 1 || limit > 100 {
		limit = 20
	}

	offset := params.Offset
	if offset < 0 {
		offset = 0
	}

	status := strings.TrimSpace(strings.ToLower(params.Status))
	if status != "" && !isAllowedStatus(status) {
		return nil, ErrInvalidInput
	}

	return s.repo.ListJobs(ctx, userID, ListParams{
		Status: status,
		Limit:  limit,
		Offset: offset,
	})
}

func (s *Service) Cancel(ctx context.Context, userID uuid.UUID, jobID string) (Job, error) {
	if userID == uuid.Nil || strings.TrimSpace(jobID) == "" {
		return Job{}, ErrInvalidInput
	}

	job, err := s.repo.GetJobByID(ctx, userID, jobID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return Job{}, ErrNotFound
		}
		return Job{}, err
	}

	if isTerminalStatus(job.Status) {
		return job, nil
	}

	updated, err := s.repo.CancelJob(ctx, userID, jobID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return Job{}, ErrNotFound
		}
		return Job{}, err
	}

	_ = s.repo.AppendEvent(ctx, updated.ID, Event{
		Status:          StatusCancelled.String(),
		Message:         "job cancelled by user",
		ProgressPercent: updated.ProgressPercent,
		Payload:         "{}",
	})

	return updated, nil
}

func (s *Service) Events(ctx context.Context, userID uuid.UUID, jobID string, params EventParams) ([]Event, error) {
	if userID == uuid.Nil || strings.TrimSpace(jobID) == "" {
		return nil, ErrInvalidInput
	}

	limit := params.Limit
	if limit < 1 || limit > 200 {
		limit = 100
	}

	_, err := s.repo.GetJobByID(ctx, userID, jobID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return nil, ErrNotFound
		}
		return nil, err
	}

	return s.repo.ListEvents(ctx, userID, jobID, EventParams{
		Limit: limit,
		After: strings.TrimSpace(params.After),
	})
}

func (s *Service) Transition(ctx context.Context, userID uuid.UUID, jobID string, input UpdateInput) (Job, error) {
	if userID == uuid.Nil || strings.TrimSpace(jobID) == "" {
		return Job{}, ErrInvalidInput
	}

	nextStatus := strings.TrimSpace(strings.ToLower(input.Status))
	if !isAllowedStatus(nextStatus) {
		return Job{}, ErrInvalidInput
	}

	current, err := s.repo.GetJobByID(ctx, userID, jobID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return Job{}, ErrNotFound
		}
		return Job{}, err
	}

	if !canTransition(current.Status, nextStatus) {
		return Job{}, ErrInvalidInput
	}

	if input.ProgressPercent < current.ProgressPercent && !isTerminalStatus(nextStatus) {
		return Job{}, ErrInvalidInput
	}

	if nextStatus == StatusCompleted.String() {
		input.ProgressPercent = 100
	}

	updated, err := s.repo.UpdateJob(ctx, userID, jobID, input)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return Job{}, ErrNotFound
		}
		return Job{}, err
	}

	_ = s.repo.AppendEvent(ctx, updated.ID, Event{
		Status:          updated.Status,
		Message:         eventMessageForStatus(updated.Status),
		ProgressPercent: updated.ProgressPercent,
		Payload:         "{}",
	})

	return updated, nil
}

func isAllowedSourceType(value string) bool {
	return slices.Contains([]string{"magnet", "torrent", "direct", "http"}, value)
}

func isAllowedStatus(value string) bool {
	return slices.Contains([]string{
		StatusQueued.String(),
		StatusPreparing.String(),
		StatusDownloading.String(),
		StatusStalled.String(),
		StatusRetrying.String(),
		StatusCompleted.String(),
		StatusFailed.String(),
		StatusCancelled.String(),
	}, value)
}

func isTerminalStatus(value string) bool {
	return slices.Contains([]string{
		StatusCompleted.String(),
		StatusFailed.String(),
		StatusCancelled.String(),
	}, strings.TrimSpace(strings.ToLower(value)))
}

func canTransition(from string, to string) bool {
	current := strings.TrimSpace(strings.ToLower(from))
	next := strings.TrimSpace(strings.ToLower(to))

	if current == next {
		return true
	}

	switch current {
	case StatusQueued.String():
		return next == StatusPreparing.String() || next == StatusCancelled.String()
	case StatusPreparing.String():
		return next == StatusDownloading.String() || next == StatusCancelled.String()
	case StatusDownloading.String():
		return next == StatusStalled.String() || next == StatusCompleted.String() || next == StatusFailed.String() || next == StatusCancelled.String()
	case StatusStalled.String():
		return next == StatusRetrying.String() || next == StatusCancelled.String()
	case StatusRetrying.String():
		return next == StatusPreparing.String() || next == StatusCancelled.String()
	default:
		return false
	}
}

func eventMessageForStatus(status string) string {
	switch strings.TrimSpace(strings.ToLower(status)) {
	case StatusPreparing.String():
		return "job preparing"
	case StatusDownloading.String():
		return "download started"
	case StatusStalled.String():
		return "download stalled"
	case StatusRetrying.String():
		return "retrying stalled download"
	case StatusCompleted.String():
		return "download completed"
	case StatusFailed.String():
		return "download failed"
	case StatusCancelled.String():
		return "download cancelled"
	default:
		return "download updated"
	}
}

func (s Status) String() string {
	return string(s)
}
