// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later

package action

import (
	"context"
	"fmt"
	"strings"

	"github.com/autobrr/autobrr/internal/domain"
	"github.com/autobrr/autobrr/pkg/errors"

	"github.com/autobrr/go-qbittorrent"
)

func (s *service) qbittorrent(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) {
	s.log.Debug().Msgf("action qBittorrent: %s", action.Name)

	client, err := s.clientSvc.GetClient(ctx, action.ClientID)
	if err != nil {
		return nil, errors.Wrap(err, "could not get client with id %d", action.ClientID)
	}
	action.Client = client

	if !client.Enabled {
		return nil, errors.New("client %s %s not enabled", client.Type, client.Name)
	}

	qbtClient := client.Client.(*qbittorrent.Client)

	if client.Settings.Rules.Enabled && !action.IgnoreRules {
		// check for active downloads and other rules
		rejections, err := s.qbittorrentCheckRulesCanDownload(ctx, action, client.Settings.Rules, qbtClient)
		if err != nil {
			return nil, errors.Wrap(err, "error checking client rules: %s", action.Name)
		}

		if len(rejections) > 0 {
			return rejections, nil
		}
	}

	if release.HasMagnetUri() {
		options, err := s.prepareQbitOptions(action)
		if err != nil {
			return nil, errors.Wrap(err, "could not prepare options")
		}

		s.log.Trace().Msgf("action qBittorrent options: %+v", options)

		if err = qbtClient.AddTorrentFromUrlCtx(ctx, release.MagnetURI, options); err != nil {
			return nil, errors.Wrap(err, "could not add torrent %s to client: %s", release.MagnetURI, client.Name)
		}

		s.log.Info().Msgf("torrent from magnet successfully added to client: '%s'", client.Name)

		return nil, nil
	}

	if err := s.downloadSvc.DownloadRelease(ctx, &release); err != nil {
		return nil, errors.Wrap(err, "could not download torrent file for release: %s", release.TorrentName)
	}

	options, err := s.prepareQbitOptions(action)
	if err != nil {
		return nil, errors.Wrap(err, "could not prepare options")
	}

	s.log.Trace().Msgf("action qBittorrent options: %+v", options)

	if err = qbtClient.AddTorrentFromFileCtx(ctx, release.TorrentTmpFile, options); err != nil {
		return nil, errors.Wrap(err, "could not add torrent %s to client: %s", release.TorrentTmpFile, client.Name)
	}

	if release.TorrentHash != "" {
		// check if torrent queueing is enabled if priority is set
		switch action.PriorityLayout {
		case domain.PriorityLayoutMax, domain.PriorityLayoutMin:
			prefs, err := qbtClient.GetAppPreferencesCtx(ctx)
			if err != nil {
				return nil, errors.Wrap(err, "could not get application preferences from client: '%s'", client.Name)
			}
			// enable queueing if it's disabled
			if !prefs.QueueingEnabled {
				if err := qbtClient.SetPreferencesQueueingEnabled(true); err != nil {
					return nil, errors.Wrap(err, "could not enable torrent queueing")
				}
				s.log.Trace().Msgf("torrent queueing was disabled, now enabled in client: '%s'", client.Name)
			}
			// set priority if queueing is enabled
			if action.PriorityLayout == domain.PriorityLayoutMax {
				if err := qbtClient.SetMaxPriorityCtx(ctx, []string{release.TorrentHash}); err != nil {
					return nil, errors.Wrap(err, "could not set torrent %s to max priority", release.TorrentHash)
				}
				s.log.Debug().Msgf("torrent with hash %s set to max priority in client: '%s'", release.TorrentHash, client.Name)

			} else { // domain.PriorityLayoutMin
				if err := qbtClient.SetMinPriorityCtx(ctx, []string{release.TorrentHash}); err != nil {
					return nil, errors.Wrap(err, "could not set torrent %s to min priority", release.TorrentHash)
				}
				s.log.Debug().Msgf("torrent with hash %s set to min priority in client: '%s'", release.TorrentHash, client.Name)
			}

		case domain.PriorityLayoutDefault:
			// do nothing as it's disabled or unset
		default:
			s.log.Warn().Msgf("unknown priority setting: '%v', no priority changes made", action.PriorityLayout)
		}
	} else {
		// add anyway if no hash
		s.log.Trace().Msg("no torrent hash provided, skipping priority setting")
	}

	if !action.Paused && !action.ReAnnounceSkip && release.TorrentHash != "" {
		opts := qbittorrent.ReannounceOptions{
			Interval:        int(action.ReAnnounceInterval),
			MaxAttempts:     int(action.ReAnnounceMaxAttempts),
			DeleteOnFailure: action.ReAnnounceDelete,
		}

		if err := qbtClient.ReannounceTorrentWithRetry(ctx, release.TorrentHash, &opts); err != nil {
			if errors.Is(err, qbittorrent.ErrReannounceTookTooLong) {
				return []string{fmt.Sprintf("re-announce took too long for hash: %s", release.TorrentHash)}, nil
			}

			return nil, errors.Wrap(err, "could not reannounce torrent: %s", release.TorrentHash)
		}
	}

	s.log.Info().Msgf("torrent with hash %s successfully added to client: '%s'", release.TorrentHash, client.Name)

	return nil, nil
}

func (s *service) prepareQbitOptions(action *domain.Action) (map[string]string, error) {
	opts := &qbittorrent.TorrentAddOptions{}

	opts.Paused = false
	if action.Paused {
		opts.Paused = true
	}
	if action.SkipHashCheck {
		opts.SkipHashCheck = true
	}
	if action.FirstLastPiecePrio {
		opts.FirstLastPiecePrio = true
	}
	switch action.ContentLayout {
	case domain.ActionContentLayoutSubfolderCreate:
		opts.ContentLayout = qbittorrent.ContentLayoutSubfolderCreate
	case domain.ActionContentLayoutSubfolderNone:
		opts.ContentLayout = qbittorrent.ContentLayoutSubfolderNone
	}
	if action.SavePath != "" {
		opts.SavePath = strings.TrimSpace(action.SavePath)
	}
	if action.DownloadPath != "" {
		opts.DownloadPath = strings.TrimSpace(action.DownloadPath)
	}
	if action.Category != "" {
		opts.Category = strings.TrimSpace(action.Category)
	}
	if action.Tags != "" {
		// Split the action.Tags string by comma
		tags := strings.Split(action.Tags, ",")

		// Create a new slice to store the trimmed tags
		trimmedTags := make([]string, 0, len(tags))

		// Iterate over the tags and trim each one
		for _, tag := range tags {
			trimmedTag := strings.TrimSpace(tag)
			trimmedTags = append(trimmedTags, trimmedTag)
		}

		// Join the trimmed tags back together with commas
		opts.Tags = strings.Join(trimmedTags, ",")
	}
	if action.LimitUploadSpeed > 0 {
		opts.LimitUploadSpeed = action.LimitUploadSpeed
	}
	if action.LimitDownloadSpeed > 0 {
		opts.LimitDownloadSpeed = action.LimitDownloadSpeed
	}
	if action.LimitRatio > 0 {
		opts.LimitRatio = action.LimitRatio
	}
	if action.LimitSeedTime > 0 {
		opts.LimitSeedTime = action.LimitSeedTime
	}

	return opts.Prepare(), nil
}

// qbittorrentCheckRulesCanDownload
func (s *service) qbittorrentCheckRulesCanDownload(ctx context.Context, action *domain.Action, rules domain.DownloadClientRules, qbt *qbittorrent.Client) ([]string, error) {
	s.log.Trace().Msgf("action qBittorrent: %s check rules", action.Name)

	// make sure it's not set to 0 by default
	if rules.MaxActiveDownloads > 0 {

		// get active downloads
		activeDownloads, err := qbt.GetTorrentsActiveDownloadsCtx(ctx)
		if err != nil {
			return nil, errors.Wrap(err, "could not fetch active downloads")
		}

		// if max active downloads reached, check speed and if lower than threshold add anyway
		if len(activeDownloads) >= rules.MaxActiveDownloads {
			// if we do not care about slow torrents then return early
			if !rules.IgnoreSlowTorrents {
				rejection := "max active downloads reached, skipping"

				s.log.Debug().Msg(rejection)

				return []string{rejection}, nil
			}

			if rules.IgnoreSlowTorrents && rules.IgnoreSlowTorrentsCondition == domain.IgnoreSlowTorrentsModeMaxReached {
				// get transfer info
				info, err := qbt.GetTransferInfoCtx(ctx)
				if err != nil {
					return nil, errors.Wrap(err, "could not get transfer info")
				}

				rejections := s.qbittorrentCheckIgnoreSlow(rules.DownloadSpeedThreshold, rules.UploadSpeedThreshold, info)
				if len(rejections) > 0 {
					return rejections, nil
				}

				s.log.Debug().Msg("active downloads are slower than set limit, lets add it")

				return nil, nil
			}
		}

		// if less, then we must check if ignore slow always which means we can't return here
	}

	// if max active downloads is unlimited or not reached, lets check if ignore slow always should be checked
	if rules.IgnoreSlowTorrents && rules.IgnoreSlowTorrentsCondition == domain.IgnoreSlowTorrentsModeAlways {
		// get transfer info
		info, err := qbt.GetTransferInfoCtx(ctx)
		if err != nil {
			return nil, errors.Wrap(err, "could not get transfer info")
		}

		rejections := s.qbittorrentCheckIgnoreSlow(rules.DownloadSpeedThreshold, rules.UploadSpeedThreshold, info)
		if len(rejections) > 0 {
			return rejections, nil
		}

		return nil, nil
	}

	return nil, nil
}

func (s *service) qbittorrentCheckIgnoreSlow(downloadSpeedThreshold int64, uploadSpeedThreshold int64, info *qbittorrent.TransferInfo) []string {
	s.log.Debug().Msgf("checking client ignore slow torrent rules: %+v", info)

	rejections := make([]string, 0)

	if downloadSpeedThreshold > 0 {
		// if current transfer speed is more than threshold return out and skip
		// DlInfoSpeed is in bytes so lets convert to KB to match DownloadSpeedThreshold
		if info.DlInfoSpeed/1024 >= downloadSpeedThreshold {
			rejection := fmt.Sprintf("total download speed (%d) above threshold: (%d), skipping", info.DlInfoSpeed/1024, downloadSpeedThreshold)

			s.log.Debug().Msg(rejection)

			rejections = append(rejections, rejection)
		}
	}

	if uploadSpeedThreshold > 0 {
		// if current transfer speed is more than threshold return out and skip
		// UpInfoSpeed is in bytes so lets convert to KB to match UploadSpeedThreshold
		if info.UpInfoSpeed/1024 >= uploadSpeedThreshold {
			rejection := fmt.Sprintf("total upload speed (%d) above threshold: (%d), skipping", info.UpInfoSpeed/1024, uploadSpeedThreshold)

			s.log.Debug().Msg(rejection)

			rejections = append(rejections, rejection)
		}
	}

	s.log.Debug().Msg("active downloads are slower than set limit, lets add it")

	return rejections
}
