package realdebrid

import (
	"bytes"
	"cmp"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/sirrobot01/decypharr/pkg/debrid/types"
	"io"
	"net/http"
	gourl "net/url"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/rs/zerolog"
	"github.com/sirrobot01/decypharr/internal/config"
	"github.com/sirrobot01/decypharr/internal/logger"
	"github.com/sirrobot01/decypharr/internal/request"
	"github.com/sirrobot01/decypharr/internal/utils"
	"github.com/sirrobot01/decypharr/pkg/rar"
)

type RealDebrid struct {
	name string
	Host string `json:"host"`

	APIKey   string
	accounts *types.Accounts

	DownloadUncached      bool
	client                *request.Client
	downloadClient        *request.Client
	repairClient          *request.Client
	autoExpiresLinksAfter time.Duration

	MountPath string
	logger    zerolog.Logger
	UnpackRar bool

	rarSemaphore    chan struct{}
	checkCached     bool
	addSamples      bool
	Profile         *types.Profile
	minimumFreeSlot int // Minimum number of active pots to maintain (used for cached stuffs, etc.)
	limit           int
}

func New(dc config.Debrid) (*RealDebrid, error) {
	rl := request.ParseRateLimit(dc.RateLimit)
	repairRl := request.ParseRateLimit(cmp.Or(dc.RepairRateLimit, dc.RateLimit))
	downloadRl := request.ParseRateLimit(cmp.Or(dc.DownloadRateLimit, dc.RateLimit))

	headers := map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
	}
	_log := logger.New(dc.Name)

	autoExpiresLinksAfter, err := time.ParseDuration(dc.AutoExpireLinksAfter)
	if autoExpiresLinksAfter == 0 || err != nil {
		autoExpiresLinksAfter = 48 * time.Hour
	}

	r := &RealDebrid{
		name:                  "realdebrid",
		Host:                  "https://api.real-debrid.com/rest/1.0",
		APIKey:                dc.APIKey,
		accounts:              types.NewAccounts(dc),
		DownloadUncached:      dc.DownloadUncached,
		autoExpiresLinksAfter: autoExpiresLinksAfter,
		UnpackRar:             dc.UnpackRar,
		client: request.New(
			request.WithHeaders(headers),
			request.WithRateLimiter(rl),
			request.WithLogger(_log),
			request.WithMaxRetries(10),
			request.WithRetryableStatus(429, 502),
			request.WithProxy(dc.Proxy),
		),
		downloadClient: request.New(
			request.WithRateLimiter(downloadRl),
			request.WithLogger(_log),
			request.WithMaxRetries(10),
			request.WithRetryableStatus(429, 447, 502),
			request.WithProxy(dc.Proxy),
		),
		repairClient: request.New(
			request.WithRateLimiter(repairRl),
			request.WithHeaders(headers),
			request.WithLogger(_log),
			request.WithMaxRetries(4),
			request.WithRetryableStatus(429, 502),
			request.WithProxy(dc.Proxy),
		),
		MountPath:       dc.Folder,
		logger:          logger.New(dc.Name),
		rarSemaphore:    make(chan struct{}, 2),
		checkCached:     dc.CheckCached,
		addSamples:      dc.AddSamples,
		minimumFreeSlot: dc.MinimumFreeSlot,
		limit:           dc.Limit,
	}

	if _, err := r.GetProfile(); err != nil {
		return nil, err
	} else {
		return r, nil
	}
}

func (r *RealDebrid) Name() string {
	return r.name
}

func (r *RealDebrid) Logger() zerolog.Logger {
	return r.logger
}

func (r *RealDebrid) getSelectedFiles(t *types.Torrent, data torrentInfo) (map[string]types.File, error) {
	files := make(map[string]types.File)
	selectedFiles := make([]types.File, 0)

	for _, f := range data.Files {
		if f.Selected == 1 {
			selectedFiles = append(selectedFiles, types.File{
				TorrentId: t.Id,
				Name:      filepath.Base(f.Path),
				Path:      filepath.Base(f.Path),
				Size:      f.Bytes,
				Id:        strconv.Itoa(f.ID),
			})
		}
	}

	if len(selectedFiles) == 0 {
		return files, nil
	}

	// Handle RARed torrents (single link, multiple files)
	if len(data.Links) == 1 && len(selectedFiles) > 1 {
		return r.handleRarArchive(t, data, selectedFiles)
	}

	// Standard case - map files to links
	if len(selectedFiles) > len(data.Links) {
		r.logger.Warn().Msgf("More files than links available: %d files, %d links for %s", len(selectedFiles), len(data.Links), t.Name)
	}

	for i, f := range selectedFiles {
		if i < len(data.Links) {
			f.Link = data.Links[i]
			files[f.Name] = f
		} else {
			r.logger.Warn().Str("file", f.Name).Msg("No link available for file")
		}
	}

	return files, nil
}

// handleRarArchive processes RAR archives with multiple files
func (r *RealDebrid) handleRarArchive(t *types.Torrent, data torrentInfo, selectedFiles []types.File) (map[string]types.File, error) {
	// This will block if 2 RAR operations are already in progress
	r.rarSemaphore <- struct{}{}
	defer func() {
		<-r.rarSemaphore
	}()

	files := make(map[string]types.File)

	if !r.UnpackRar {
		r.logger.Debug().Msgf("RAR file detected, but unpacking is disabled: %s", t.Name)
		// Create a single file representing the RAR archive
		file := types.File{
			TorrentId: t.Id,
			Id:        "0",
			Name:      t.Name + ".rar",
			Size:      0,
			IsRar:     true,
			ByteRange: nil,
			Path:      t.Name + ".rar",
			Link:      data.Links[0],
			Generated: time.Now(),
		}
		files[file.Name] = file
		return files, nil
	}

	r.logger.Info().Msgf("RAR file detected, unpacking: %s", t.Name)
	linkFile := &types.File{TorrentId: t.Id, Link: data.Links[0]}
	downloadLinkObj, err := r.GetDownloadLink(t, linkFile)

	if err != nil {
		return nil, fmt.Errorf("failed to get download link for RAR file: %w", err)
	}

	dlLink := downloadLinkObj.DownloadLink
	reader, err := rar.NewReader(dlLink)

	if err != nil {
		return nil, fmt.Errorf("failed to create RAR reader: %w", err)
	}

	rarFiles, err := reader.GetFiles()

	if err != nil {
		return nil, fmt.Errorf("failed to read RAR files: %w", err)
	}

	// Create lookup map for faster matching
	fileMap := make(map[string]*types.File)
	for i := range selectedFiles {
		// RD converts special chars to '_' for RAR file paths
		// @TODO: there might be more special chars to replace
		safeName := strings.NewReplacer("|", "_", "\"", "_", "\\", "_", "?", "_", "*", "_", ":", "_", "<", "_", ">", "_").Replace(selectedFiles[i].Name)
		fileMap[safeName] = &selectedFiles[i]
	}

	now := time.Now()

	for _, rarFile := range rarFiles {
		if file, exists := fileMap[rarFile.Name()]; exists {
			file.IsRar = true
			file.ByteRange = rarFile.ByteRange()
			file.Link = data.Links[0]
			file.Generated = now
			files[file.Name] = *file
		} else if !rarFile.IsDirectory {
			r.logger.Warn().Msgf("RAR file %s not found in torrent files", rarFile.Name())
		}
	}

	return files, nil
}

// getTorrentFiles returns a list of torrent files from the torrent info
// validate is used to determine if the files should be validated
// if validate is false, selected files will be returned
func (r *RealDebrid) getTorrentFiles(t *types.Torrent, data torrentInfo) map[string]types.File {
	files := make(map[string]types.File)
	cfg := config.Get()
	idx := 0

	for _, f := range data.Files {
		name := filepath.Base(f.Path)
		if !r.addSamples && utils.IsSampleFile(f.Path) {
			// Skip sample files
			continue
		}

		if !cfg.IsAllowedFile(name) {
			continue
		}
		if !cfg.IsSizeAllowed(f.Bytes) {
			continue
		}

		file := types.File{
			TorrentId: t.Id,
			Name:      name,
			Path:      name,
			Size:      f.Bytes,
			Id:        strconv.Itoa(f.ID),
		}
		files[name] = file
		idx++
	}
	return files
}

func (r *RealDebrid) IsAvailable(hashes []string) map[string]bool {
	// Check if the infohashes are available in the local cache
	result := make(map[string]bool)

	// Divide hashes into groups of 100
	for i := 0; i < len(hashes); i += 200 {
		end := i + 200
		if end > len(hashes) {
			end = len(hashes)
		}

		// Filter out empty strings
		validHashes := make([]string, 0, end-i)
		for _, hash := range hashes[i:end] {
			if hash != "" {
				validHashes = append(validHashes, hash)
			}
		}

		// If no valid hashes in this batch, continue to the next batch
		if len(validHashes) == 0 {
			continue
		}

		hashStr := strings.Join(validHashes, "/")
		url := fmt.Sprintf("%s/torrents/instantAvailability/%s", r.Host, hashStr)
		req, _ := http.NewRequest(http.MethodGet, url, nil)
		resp, err := r.client.MakeRequest(req)
		if err != nil {
			r.logger.Error().Err(err).Msgf("Error checking availability")
			return result
		}
		var data AvailabilityResponse
		err = json.Unmarshal(resp, &data)
		if err != nil {
			r.logger.Error().Err(err).Msgf("Error marshalling availability")
			return result
		}
		for _, h := range hashes[i:end] {
			hosters, exists := data[strings.ToLower(h)]
			if exists && len(hosters.Rd) > 0 {
				result[h] = true
			}
		}
	}
	return result
}

func (r *RealDebrid) SubmitMagnet(t *types.Torrent) (*types.Torrent, error) {
	if t.Magnet.IsTorrent() {
		return r.addTorrent(t)
	}
	return r.addMagnet(t)
}

func (r *RealDebrid) addTorrent(t *types.Torrent) (*types.Torrent, error) {
	url := fmt.Sprintf("%s/torrents/addTorrent", r.Host)
	var data AddMagnetSchema
	req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(t.Magnet.File))

	if err != nil {
		return nil, err
	}
	req.Header.Add("Content-Type", "application/x-bittorrent")
	resp, err := r.client.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		// Handle multiple_downloads

		if resp.StatusCode == 509 {
			return nil, utils.TooManyActiveDownloadsError
		}

		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("realdebrid API error: Status: %d || Body: %s", resp.StatusCode, string(bodyBytes))
	}
	defer resp.Body.Close()
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("reading response body: %w", err)
	}
	if err = json.Unmarshal(bodyBytes, &data); err != nil {
		return nil, err
	}
	t.Id = data.Id
	t.Debrid = r.name
	t.MountPath = r.MountPath
	return t, nil
}

func (r *RealDebrid) addMagnet(t *types.Torrent) (*types.Torrent, error) {
	url := fmt.Sprintf("%s/torrents/addMagnet", r.Host)
	payload := gourl.Values{
		"magnet": {t.Magnet.Link},
	}
	var data AddMagnetSchema
	req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
	resp, err := r.client.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		// Handle multiple_downloads

		if resp.StatusCode == 509 {
			return nil, utils.TooManyActiveDownloadsError
		}

		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("realdebrid API error: Status: %d || Body: %s", resp.StatusCode, string(bodyBytes))
	}
	defer resp.Body.Close()
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("reading response body: %w", err)
	}
	if err = json.Unmarshal(bodyBytes, &data); err != nil {
		return nil, err
	}
	t.Id = data.Id
	t.Debrid = r.name
	t.MountPath = r.MountPath
	return t, nil
}

func (r *RealDebrid) GetTorrent(torrentId string) (*types.Torrent, error) {
	url := fmt.Sprintf("%s/torrents/info/%s", r.Host, torrentId)
	req, _ := http.NewRequest(http.MethodGet, url, nil)
	resp, err := r.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("reading response body: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		if resp.StatusCode == http.StatusNotFound {
			return nil, utils.TorrentNotFoundError
		}
		return nil, fmt.Errorf("realdebrid API error: Status: %d || Body: %s", resp.StatusCode, string(bodyBytes))
	}
	var data torrentInfo
	err = json.Unmarshal(bodyBytes, &data)
	if err != nil {
		return nil, err
	}
	t := &types.Torrent{
		Id:               data.ID,
		Name:             data.Filename,
		Bytes:            data.Bytes,
		Folder:           data.OriginalFilename,
		Progress:         data.Progress,
		Speed:            data.Speed,
		Seeders:          data.Seeders,
		Added:            data.Added,
		Status:           data.Status,
		Filename:         data.Filename,
		OriginalFilename: data.OriginalFilename,
		Links:            data.Links,
		Debrid:           r.name,
		MountPath:        r.MountPath,
	}
	t.Files = r.getTorrentFiles(t, data) // Get selected files
	return t, nil
}

func (r *RealDebrid) UpdateTorrent(t *types.Torrent) error {
	url := fmt.Sprintf("%s/torrents/info/%s", r.Host, t.Id)
	req, _ := http.NewRequest(http.MethodGet, url, nil)
	resp, err := r.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("reading response body: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		if resp.StatusCode == http.StatusNotFound {
			return utils.TorrentNotFoundError
		}
		return fmt.Errorf("realdebrid API error: Status: %d || Body: %s", resp.StatusCode, string(bodyBytes))
	}
	var data torrentInfo
	err = json.Unmarshal(bodyBytes, &data)
	if err != nil {
		return err
	}
	t.Name = data.Filename
	t.Bytes = data.Bytes
	t.Folder = data.OriginalFilename
	t.Progress = data.Progress
	t.Status = data.Status
	t.Speed = data.Speed
	t.Seeders = data.Seeders
	t.Filename = data.Filename
	t.OriginalFilename = data.OriginalFilename
	t.Links = data.Links
	t.MountPath = r.MountPath
	t.Debrid = r.name
	t.Added = data.Added
	t.Files, _ = r.getSelectedFiles(t, data) // Get selected files

	return nil
}

func (r *RealDebrid) CheckStatus(t *types.Torrent) (*types.Torrent, error) {
	url := fmt.Sprintf("%s/torrents/info/%s", r.Host, t.Id)
	req, _ := http.NewRequest(http.MethodGet, url, nil)
	for {
		resp, err := r.client.MakeRequest(req)
		if err != nil {
			r.logger.Info().Msgf("ERROR Checking file: %v", err)
			return t, err
		}
		var data torrentInfo
		if err = json.Unmarshal(resp, &data); err != nil {
			return t, err
		}
		status := data.Status
		t.Name = data.Filename // Important because some magnet changes the name
		t.Folder = data.OriginalFilename
		t.Filename = data.Filename
		t.OriginalFilename = data.OriginalFilename
		t.Bytes = data.Bytes
		t.Progress = data.Progress
		t.Speed = data.Speed
		t.Seeders = data.Seeders
		t.Links = data.Links
		t.Status = status
		t.Debrid = r.name
		t.MountPath = r.MountPath
		if status == "waiting_files_selection" {
			t.Files = r.getTorrentFiles(t, data)
			if len(t.Files) == 0 {
				return t, fmt.Errorf("no video files found")
			}
			filesId := make([]string, 0)
			for _, f := range t.Files {
				filesId = append(filesId, f.Id)
			}
			p := gourl.Values{
				"files": {strings.Join(filesId, ",")},
			}
			payload := strings.NewReader(p.Encode())
			req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/torrents/selectFiles/%s", r.Host, t.Id), payload)
			res, err := r.client.Do(req)
			if err != nil {
				return t, err
			}
			if res.StatusCode != http.StatusNoContent {
				if res.StatusCode == 509 {
					return nil, utils.TooManyActiveDownloadsError
				}
				return t, fmt.Errorf("realdebrid API error: Status: %d", res.StatusCode)
			}
		} else if status == "downloaded" {
			t.Files, err = r.getSelectedFiles(t, data) // Get selected files
			if err != nil {
				return t, err
			}

			r.logger.Info().Msgf("Torrent: %s downloaded to RD", t.Name)
			return t, nil
		} else if utils.Contains(r.GetDownloadingStatus(), status) {
			if !t.DownloadUncached {
				return t, fmt.Errorf("torrent: %s not cached", t.Name)
			}
			return t, nil
		} else {
			return t, fmt.Errorf("torrent: %s has error: %s", t.Name, status)
		}

	}
}

func (r *RealDebrid) DeleteTorrent(torrentId string) error {
	url := fmt.Sprintf("%s/torrents/delete/%s", r.Host, torrentId)
	req, _ := http.NewRequest(http.MethodDelete, url, nil)
	if _, err := r.client.MakeRequest(req); err != nil {
		return err
	}
	r.logger.Info().Msgf("Torrent: %s deleted from RD", torrentId)
	return nil
}

func (r *RealDebrid) GetFileDownloadLinks(t *types.Torrent) error {
	var wg sync.WaitGroup
	var mu sync.Mutex
	var firstErr error

	files := make(map[string]types.File)
	links := make(map[string]*types.DownloadLink)

	_files := t.GetFiles()
	wg.Add(len(_files))

	for _, f := range _files {
		go func(file types.File) {
			defer wg.Done()

			link, err := r.GetDownloadLink(t, &file)
			if err != nil {
				mu.Lock()
				if firstErr == nil {
					firstErr = err
				}
				mu.Unlock()
				return
			}
			if link == nil {
				mu.Lock()
				if firstErr == nil {
					firstErr = fmt.Errorf("realdebrid API error: download link not found for file %s", file.Name)
				}
				mu.Unlock()
				return
			}

			file.DownloadLink = link

			mu.Lock()
			files[file.Name] = file
			links[link.Link] = link
			mu.Unlock()
		}(f)
	}

	wg.Wait()

	if firstErr != nil {
		return firstErr
	}

	// Add links to cache
	r.accounts.SetDownloadLinks(links)
	t.Files = files
	return nil
}

func (r *RealDebrid) CheckLink(link string) error {
	url := fmt.Sprintf("%s/unrestrict/check", r.Host)
	payload := gourl.Values{
		"link": {link},
	}
	req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
	resp, err := r.repairClient.Do(req)
	if err != nil {
		return err
	}
	if resp.StatusCode == http.StatusNotFound {
		return utils.HosterUnavailableError // File has been removed
	}
	return nil
}

func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, error) {
	url := fmt.Sprintf("%s/unrestrict/link/", r.Host)
	_link := file.Link
	if strings.HasPrefix(file.Link, "https://real-debrid.com/d/") && len(file.Link) > 39 {
		_link = file.Link[0:39]
	}
	payload := gourl.Values{
		"link": {_link},
	}
	req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
	resp, err := r.downloadClient.Do(req)

	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		// Read the response body to get the error message
		b, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, err
		}
		var data ErrorResponse
		if err = json.Unmarshal(b, &data); err != nil {
			return nil, fmt.Errorf("error unmarshalling %d || %s \n %s", resp.StatusCode, err, string(b))
		}
		switch data.ErrorCode {
		case 19:
			return nil, utils.HosterUnavailableError // File has been removed
		case 23:
			return nil, utils.TrafficExceededError
		case 24:
			return nil, utils.HosterUnavailableError // Link has been nerfed
		case 34:
			return nil, utils.TrafficExceededError // traffic exceeded
		case 35:
			return nil, utils.HosterUnavailableError
		case 36:
			return nil, utils.TrafficExceededError // traffic exceeded
		default:
			return nil, fmt.Errorf("realdebrid API error: Status: %d || Code: %d", resp.StatusCode, data.ErrorCode)
		}
	}
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	var data UnrestrictResponse
	if err = json.Unmarshal(b, &data); err != nil {
		return nil, fmt.Errorf("realdebrid API error: Error unmarshalling response: %w", err)
	}
	if data.Download == "" {
		return nil, fmt.Errorf("realdebrid API error: download link not found")
	}
	now := time.Now()
	return &types.DownloadLink{
		Filename:     data.Filename,
		Size:         data.Filesize,
		Link:         data.Link,
		DownloadLink: data.Download,
		Generated:    now,
		ExpiresAt:    now.Add(r.autoExpiresLinksAfter),
	}, nil

}

func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) {

	accounts := r.accounts.All()

	for _, account := range accounts {
		r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", account.Token))
		downloadLink, err := r._getDownloadLink(file)

		if err == nil {
			return downloadLink, nil
		}

		retries := 0
		if errors.Is(err, utils.TrafficExceededError) {
			// Retries generating
			retries = 5
		} else {
			// If the error is not traffic exceeded, return the error
			return nil, err
		}
		backOff := 1 * time.Second
		for retries > 0 {
			downloadLink, err = r._getDownloadLink(file)
			if err == nil {
				return downloadLink, nil
			}
			if !errors.Is(err, utils.TrafficExceededError) {
				return nil, err
			}
			// Add a delay before retrying
			time.Sleep(backOff)
			backOff *= 2 // Exponential backoff
			retries--
		}
	}
	return nil, fmt.Errorf("realdebrid API error: download link not found")
}

func (r *RealDebrid) getTorrents(offset int, limit int) (int, []*types.Torrent, error) {
	url := fmt.Sprintf("%s/torrents?limit=%d", r.Host, limit)
	torrents := make([]*types.Torrent, 0)
	if offset > 0 {
		url = fmt.Sprintf("%s&offset=%d", url, offset)
	}
	req, _ := http.NewRequest(http.MethodGet, url, nil)
	resp, err := r.client.Do(req)

	if err != nil {
		return 0, torrents, err
	}

	if resp.StatusCode == http.StatusNoContent {
		return 0, torrents, nil
	}

	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return 0, torrents, fmt.Errorf("realdebrid API error: %d", resp.StatusCode)
	}

	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return 0, torrents, err
	}
	totalItems, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
	var data []TorrentsResponse
	if err = json.Unmarshal(body, &data); err != nil {
		return 0, torrents, err
	}
	filenames := map[string]struct{}{}
	for _, t := range data {
		if t.Status != "downloaded" {
			continue
		}
		torrents = append(torrents, &types.Torrent{
			Id:               t.Id,
			Name:             t.Filename,
			Bytes:            t.Bytes,
			Progress:         t.Progress,
			Status:           t.Status,
			Filename:         t.Filename,
			OriginalFilename: t.Filename,
			Links:            t.Links,
			Files:            make(map[string]types.File),
			InfoHash:         t.Hash,
			Debrid:           r.name,
			MountPath:        r.MountPath,
			Added:            t.Added.Format(time.RFC3339),
		})
		filenames[t.Filename] = struct{}{}
	}
	return totalItems, torrents, nil
}

func (r *RealDebrid) GetTorrents() ([]*types.Torrent, error) {
	limit := 5000
	if r.limit != 0 {
		limit = r.limit
	}
	hardLimit := r.limit

	// Get first batch and total count
	allTorrents := make([]*types.Torrent, 0)
	var fetchError error
	offset := 0
	for {
		// Fetch next batch of torrents
		_, torrents, err := r.getTorrents(offset, limit)
		if err != nil {
			fetchError = err
			break
		}
		totalTorrents := len(torrents)
		if totalTorrents == 0 {
			break
		}
		allTorrents = append(allTorrents, torrents...)
		offset += totalTorrents
		if hardLimit != 0 && len(allTorrents) >= hardLimit {
			// If hard limit is set, stop fetching more torrents
			break
		}
	}

	if fetchError != nil {
		return nil, fetchError
	}

	return allTorrents, nil
}

func (r *RealDebrid) GetDownloadLinks() (map[string]*types.DownloadLink, error) {
	links := make(map[string]*types.DownloadLink)
	offset := 0
	limit := 1000

	accounts := r.accounts.All()

	if len(accounts) < 1 {
		// No active download keys. It's likely that the key has reached bandwidth limit
		return links, fmt.Errorf("no active download keys")
	}
	activeAccount := accounts[0]
	r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", activeAccount.Token))
	for {
		dl, err := r._getDownloads(offset, limit)
		if err != nil {
			break
		}
		if len(dl) == 0 {
			break
		}

		for _, d := range dl {
			if _, exists := links[d.Link]; exists {
				// This is ordered by date, so we can skip the rest
				continue
			}
			links[d.Link] = &d
		}

		offset += len(dl)
	}

	return links, nil
}

func (r *RealDebrid) _getDownloads(offset int, limit int) ([]types.DownloadLink, error) {
	url := fmt.Sprintf("%s/downloads?limit=%d", r.Host, limit)
	if offset > 0 {
		url = fmt.Sprintf("%s&offset=%d", url, offset)
	}
	req, _ := http.NewRequest(http.MethodGet, url, nil)
	resp, err := r.downloadClient.MakeRequest(req)
	if err != nil {
		return nil, err
	}
	var data []DownloadsResponse
	if err = json.Unmarshal(resp, &data); err != nil {
		return nil, err
	}
	links := make([]types.DownloadLink, 0)
	for _, d := range data {
		links = append(links, types.DownloadLink{
			Filename:     d.Filename,
			Size:         d.Filesize,
			Link:         d.Link,
			DownloadLink: d.Download,
			Generated:    d.Generated,
			ExpiresAt:    d.Generated.Add(r.autoExpiresLinksAfter),
			Id:           d.Id,
		})

	}
	return links, nil
}

func (r *RealDebrid) GetDownloadingStatus() []string {
	return []string{"downloading", "magnet_conversion", "queued", "compressing", "uploading"}
}

func (r *RealDebrid) GetDownloadUncached() bool {
	return r.DownloadUncached
}

func (r *RealDebrid) GetMountPath() string {
	return r.MountPath
}

func (r *RealDebrid) DeleteDownloadLink(linkId string) error {
	url := fmt.Sprintf("%s/downloads/delete/%s", r.Host, linkId)
	req, _ := http.NewRequest(http.MethodDelete, url, nil)
	if _, err := r.downloadClient.MakeRequest(req); err != nil {
		return err
	}
	return nil
}

func (r *RealDebrid) GetProfile() (*types.Profile, error) {
	if r.Profile != nil {
		return r.Profile, nil
	}
	url := fmt.Sprintf("%s/user", r.Host)
	req, _ := http.NewRequest(http.MethodGet, url, nil)

	resp, err := r.client.MakeRequest(req)
	if err != nil {
		return nil, err
	}
	var data profileResponse
	if json.Unmarshal(resp, &data) != nil {
		return nil, err
	}
	profile := &types.Profile{
		Id:         data.Id,
		Username:   data.Username,
		Email:      data.Email,
		Points:     data.Points,
		Premium:    data.Premium,
		Expiration: data.Expiration,
		Type:       data.Type,
	}
	r.Profile = profile
	return profile, nil
}

func (r *RealDebrid) GetAvailableSlots() (int, error) {
	url := fmt.Sprintf("%s/torrents/activeCount", r.Host)
	req, _ := http.NewRequest(http.MethodGet, url, nil)
	resp, err := r.client.MakeRequest(req)
	if err != nil {
		return 0, nil
	}
	var data AvailableSlotsResponse
	if json.Unmarshal(resp, &data) != nil {
		return 0, fmt.Errorf("error unmarshalling available slots response: %w", err)
	}
	return data.TotalSlots - data.ActiveSlots - r.minimumFreeSlot, nil // Ensure we maintain minimum active pots
}

func (r *RealDebrid) Accounts() *types.Accounts {
	return r.accounts
}
