package video

import (
	"context"
	"encoding/json"
	"errors"
	"net/url"
	"strings"

	"github.com/google/uuid"
	"gorm.io/gorm"
	"stream.api/internal/database/model"
)

var (
	ErrUserNotFound          = errors.New("user not found")
	ErrAdTemplateNotFound    = errors.New("ad template not found")
	ErrJobServiceUnavailable = errors.New("job service is unavailable")
)

type Service struct {
	db         *gorm.DB
	jobService JobService
}

type CreateVideoInput struct {
	UserID       string
	Title        string
	Description  *string
	URL          string
	Size         int64
	Duration     int32
	Format       string
	AdTemplateID *string
}

type CreateVideoResult struct {
	Video *model.Video
	Job   *Job
}

func NewService(db *gorm.DB, jobService JobService) *Service {
	return &Service{db: db, jobService: jobService}
}

func (s *Service) JobService() JobService {
	if s == nil {
		return nil
	}
	return s.jobService
}

func (s *Service) CreateVideo(ctx context.Context, input CreateVideoInput) (*CreateVideoResult, error) {
	if s == nil || s.db == nil {
		return nil, gorm.ErrInvalidDB
	}

	userID := strings.TrimSpace(input.UserID)
	if userID == "" {
		return nil, ErrUserNotFound
	}

	var user model.User
	if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrUserNotFound
		}
		return nil, err
	}

	title := strings.TrimSpace(input.Title)
	videoURL := strings.TrimSpace(input.URL)
	format := strings.TrimSpace(input.Format)
	statusValue := "processing"
	processingStatus := "PENDING"
	storageType := detectStorageType(videoURL)

	video := &model.Video{
		ID:               uuid.NewString(),
		UserID:           user.ID,
		Name:             title,
		Title:            title,
		Description:      nullableTrimmedString(input.Description),
		URL:              videoURL,
		Size:             input.Size,
		Duration:         input.Duration,
		Format:           format,
		Status:           model.StringPtr(statusValue),
		ProcessingStatus: model.StringPtr(processingStatus),
		StorageType:      model.StringPtr(storageType),
	}
	if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(video).Error; err != nil {
			return err
		}
		if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
			return err
		}
		return saveVideoAdConfig(ctx, tx, video, user.ID, input.AdTemplateID)
	}); err != nil {
		return nil, err
	}

	if s.jobService == nil {
		_ = markVideoJobFailed(ctx, s.db, video.ID)
		return nil, ErrJobServiceUnavailable
	}

	jobPayload, err := buildJobPayload(video.ID, user.ID, videoURL, format)
	if err != nil {
		_ = markVideoJobFailed(ctx, s.db, video.ID)
		return nil, err
	}

	job, err := s.jobService.CreateJob(ctx, user.ID, video.ID, title, jobPayload, 0, 0)
	if err != nil {
		_ = markVideoJobFailed(ctx, s.db, video.ID)
		return nil, err
	}

	return &CreateVideoResult{Video: video, Job: job}, nil
}

func (s *Service) ListJobs(ctx context.Context, offset, limit int) (*PaginatedJobs, error) {
	if s == nil || s.jobService == nil {
		return nil, ErrJobServiceUnavailable
	}
	return s.jobService.ListJobs(ctx, offset, limit)
}

func (s *Service) ListJobsByAgent(ctx context.Context, agentID string, offset, limit int) (*PaginatedJobs, error) {
	if s == nil || s.jobService == nil {
		return nil, ErrJobServiceUnavailable
	}
	return s.jobService.ListJobsByAgent(ctx, agentID, offset, limit)
}

func (s *Service) ListJobsByCursor(ctx context.Context, agentID string, cursor string, pageSize int) (*PaginatedJobs, error) {
	if s == nil || s.jobService == nil {
		return nil, ErrJobServiceUnavailable
	}
	return s.jobService.ListJobsByCursor(ctx, agentID, cursor, pageSize)
}

func (s *Service) GetJob(ctx context.Context, id string) (*Job, error) {
	if s == nil || s.jobService == nil {
		return nil, ErrJobServiceUnavailable
	}
	return s.jobService.GetJob(ctx, id)
}

func (s *Service) CreateJob(ctx context.Context, userID string, videoID string, name string, config []byte, priority int, timeLimit int64) (*Job, error) {
	if s == nil || s.jobService == nil {
		return nil, ErrJobServiceUnavailable
	}
	return s.jobService.CreateJob(ctx, userID, videoID, name, config, priority, timeLimit)
}

func (s *Service) CancelJob(ctx context.Context, id string) error {
	if s == nil || s.jobService == nil {
		return ErrJobServiceUnavailable
	}
	return s.jobService.CancelJob(ctx, id)
}

func (s *Service) RetryJob(ctx context.Context, id string) (*Job, error) {
	if s == nil || s.jobService == nil {
		return nil, ErrJobServiceUnavailable
	}
	return s.jobService.RetryJob(ctx, id)
}

func buildJobPayload(videoID, userID, videoURL, format string) ([]byte, error) {
	return json.Marshal(map[string]any{
		"video_id":   videoID,
		"user_id":    userID,
		"input_url":  videoURL,
		"source_url": videoURL,
		"format":     format,
	})
}

func saveVideoAdConfig(ctx context.Context, tx *gorm.DB, video *model.Video, userID string, adTemplateID *string) error {
	if video == nil || adTemplateID == nil {
		return nil
	}

	trimmed := strings.TrimSpace(*adTemplateID)
	if trimmed == "" {
		if err := tx.WithContext(ctx).Model(&model.Video{}).Where("id = ?", video.ID).Update("ad_id", nil).Error; err != nil {
			return err
		}
		video.AdID = nil
		return nil
	}

	var template model.AdTemplate
	if err := tx.WithContext(ctx).Select("id").Where("id = ? AND user_id = ?", trimmed, userID).First(&template).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return ErrAdTemplateNotFound
		}
		return err
	}

	if err := tx.WithContext(ctx).Model(&model.Video{}).Where("id = ?", video.ID).Update("ad_id", template.ID).Error; err != nil {
		return err
	}
	video.AdID = &template.ID
	return nil
}

func markVideoJobFailed(ctx context.Context, db *gorm.DB, videoID string) error {
	if db == nil {
		return nil
	}
	return db.WithContext(ctx).
		Model(&model.Video{}).
		Where("id = ?", strings.TrimSpace(videoID)).
		Updates(map[string]any{"status": "failed", "processing_status": "FAILED"}).Error
}

func detectStorageType(rawURL string) string {
	if shouldDeleteStoredObject(rawURL) {
		return "S3"
	}
	return "WORKER"
}

func shouldDeleteStoredObject(rawURL string) bool {
	trimmed := strings.TrimSpace(rawURL)
	if trimmed == "" {
		return false
	}
	parsed, err := url.Parse(trimmed)
	if err != nil {
		return !strings.HasPrefix(trimmed, "/")
	}
	return parsed.Scheme == "" && parsed.Host == "" && !strings.HasPrefix(trimmed, "/")
}

func nullableTrimmedString(value *string) *string {
	if value == nil {
		return nil
	}
	trimmed := strings.TrimSpace(*value)
	if trimmed == "" {
		return nil
	}
	return &trimmed
}
