package debrid

import (
	"errors"
	"fmt"
	"github.com/sirrobot01/decypharr/internal/request"
	"github.com/sirrobot01/decypharr/pkg/debrid/types"
	"sync"
	"time"
)

type linkCache struct {
	Id        string
	link      string
	accountId string
	expiresAt time.Time
}

type downloadLinkCache struct {
	data map[string]linkCache
	mu   sync.Mutex
}

func newDownloadLinkCache() *downloadLinkCache {
	return &downloadLinkCache{
		data: make(map[string]linkCache),
	}
}

func (c *downloadLinkCache) reset() {
	c.mu.Lock()
	c.data = make(map[string]linkCache)
	c.mu.Unlock()
}

func (c *downloadLinkCache) Load(key string) (linkCache, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	dl, ok := c.data[key]
	return dl, ok
}
func (c *downloadLinkCache) Store(key string, value linkCache) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.data[key] = value
}
func (c *downloadLinkCache) Delete(key string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.data, key)
}

type downloadLinkRequest struct {
	result string
	err    error
	done   chan struct{}
}

func newDownloadLinkRequest() *downloadLinkRequest {
	return &downloadLinkRequest{
		done: make(chan struct{}),
	}
}

func (r *downloadLinkRequest) Complete(result string, err error) {
	r.result = result
	r.err = err
	close(r.done)
}

func (r *downloadLinkRequest) Wait() (string, error) {
	<-r.done
	return r.result, r.err
}

func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string, error) {
	// Check link cache
	if dl := c.checkDownloadLink(fileLink); dl != "" {
		return dl, nil
	}

	if req, inFlight := c.downloadLinkRequests.Load(fileLink); inFlight {
		// Wait for the other request to complete and use its result
		result := req.(*downloadLinkRequest)
		return result.Wait()
	}

	// Create a new request object
	req := newDownloadLinkRequest()
	c.downloadLinkRequests.Store(fileLink, req)

	downloadLink, err := c.fetchDownloadLink(torrentName, filename, fileLink)

	// Complete the request and remove it from the map
	req.Complete(downloadLink, err)
	c.downloadLinkRequests.Delete(fileLink)

	return downloadLink, err
}

func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (string, error) {
	ct := c.GetTorrentByName(torrentName)
	if ct == nil {
		return "", fmt.Errorf("torrent not found")
	}
	file := ct.Files[filename]

	if file.Link == "" {
		// file link is empty, refresh the torrent to get restricted links
		ct = c.refreshTorrent(file.TorrentId) // Refresh the torrent from the debrid
		if ct == nil {
			return "", fmt.Errorf("failed to refresh torrent")
		} else {
			file = ct.Files[filename]
		}
	}

	// If file.Link is still empty, return
	if file.Link == "" {
		// Try to reinsert the torrent?
		newCt, err := c.reInsertTorrent(ct)
		if err != nil {
			return "", fmt.Errorf("failed to reinsert torrent. %w", err)
		}
		ct = newCt
		file = ct.Files[filename]
	}

	c.logger.Trace().Msgf("Getting download link for %s(%s)", filename, file.Link)
	downloadLink, err := c.client.GetDownloadLink(ct.Torrent, &file)
	if err != nil {
		if errors.Is(err, request.HosterUnavailableError) {
			newCt, err := c.reInsertTorrent(ct)
			if err != nil {
				return "", fmt.Errorf("failed to reinsert torrent: %w", err)
			}
			ct = newCt
			file = ct.Files[filename]
			// Retry getting the download link
			downloadLink, err = c.client.GetDownloadLink(ct.Torrent, &file)
			if err != nil {
				return "", err
			}
			if downloadLink == nil {
				return "", fmt.Errorf("download link is empty for")
			}
			c.updateDownloadLink(downloadLink)
			return "", nil
		} else if errors.Is(err, request.TrafficExceededError) {
			// This is likely a fair usage limit error
			return "", err
		} else {
			return "", fmt.Errorf("failed to get download link: %w", err)
		}
	}
	if downloadLink == nil {
		return "", fmt.Errorf("download link is empty")
	}
	c.updateDownloadLink(downloadLink)
	return downloadLink.DownloadLink, nil
}

func (c *Cache) GenerateDownloadLinks(t CachedTorrent) {
	if err := c.client.GenerateDownloadLinks(t.Torrent); err != nil {
		c.logger.Error().Err(err).Str("torrent", t.Name).Msg("Failed to generate download links")
		return
	}
	for _, file := range t.Files {
		if file.DownloadLink != nil {
			c.updateDownloadLink(file.DownloadLink)
		}
	}
	c.setTorrent(t, nil)
}

func (c *Cache) updateDownloadLink(dl *types.DownloadLink) {
	c.downloadLinks.Store(dl.Link, linkCache{
		Id:        dl.Id,
		link:      dl.DownloadLink,
		expiresAt: time.Now().Add(c.autoExpiresLinksAfterDuration),
		accountId: dl.AccountId,
	})
}

func (c *Cache) checkDownloadLink(link string) string {
	if dl, ok := c.downloadLinks.Load(link); ok {
		if dl.expiresAt.After(time.Now()) && !c.IsDownloadLinkInvalid(dl.link) {
			return dl.link
		}
	}
	return ""
}

func (c *Cache) MarkDownloadLinkAsInvalid(link, downloadLink, reason string) {
	c.invalidDownloadLinks.Store(downloadLink, reason)
	// Remove the download api key from active
	if reason == "bandwidth_exceeded" {
		if dl, ok := c.downloadLinks.Load(link); ok {
			if dl.accountId != "" && dl.link == downloadLink {
				c.client.DisableAccount(dl.accountId)
			}
		}
	}
	c.removeDownloadLink(link)
}

func (c *Cache) removeDownloadLink(link string) {
	if dl, ok := c.downloadLinks.Load(link); ok {
		// Delete dl from cache
		c.downloadLinks.Delete(link)
		// Delete dl from debrid
		if dl.Id != "" {
			_ = c.client.DeleteDownloadLink(dl.Id)
		}
	}
}

func (c *Cache) IsDownloadLinkInvalid(downloadLink string) bool {
	if reason, ok := c.invalidDownloadLinks.Load(downloadLink); ok {
		c.logger.Debug().Msgf("Download link %s is invalid: %s", downloadLink, reason)
		return true
	}
	return false
}
