package download

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"time"
)

type QBittorrentClient struct {
	baseURL  string
	username string
	password string
	client   *http.Client
	mu       sync.Mutex
	sid      *http.Cookie
}

func NewQBittorrentClient(baseURL, password string) *QBittorrentClient {
	return &QBittorrentClient{
		baseURL:  strings.TrimRight(baseURL, "/"),
		username: "admin",
		password: password,
		client:   &http.Client{Timeout: 30 * time.Second},
	}
}

func (q *QBittorrentClient) login(ctx context.Context) error {
	q.mu.Lock()
	defer q.mu.Unlock()

	form := url.Values{}
	form.Set("username", q.username)
	form.Set("password", q.password)

	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		q.baseURL+"/api/v2/auth/login", strings.NewReader(form.Encode()))
	if err != nil {
		return fmt.Errorf("qbittorrent login request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := q.client.Do(req)
	if err != nil {
		return fmt.Errorf("qbittorrent login: %w", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	if !strings.Contains(string(body), "Ok.") {
		return fmt.Errorf("qbittorrent login failed: %s", string(body))
	}

	for _, cookie := range resp.Cookies() {
		if cookie.Name == "SID" {
			q.sid = cookie
			return nil
		}
	}

	return fmt.Errorf("qbittorrent login: no SID cookie in response")
}

func (q *QBittorrentClient) doAuthenticated(ctx context.Context, method, path string, body url.Values) (*http.Response, error) {
	var bodyReader io.Reader
	if body != nil {
		bodyReader = strings.NewReader(body.Encode())
	}

	req, err := http.NewRequestWithContext(ctx, method, q.baseURL+path, bodyReader)
	if err != nil {
		return nil, err
	}

	if body != nil {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}

	if q.sid != nil {
		req.AddCookie(q.sid)
	}

	resp, err := q.client.Do(req)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode == http.StatusForbidden {
		resp.Body.Close()
		if err := q.login(ctx); err != nil {
			return nil, fmt.Errorf("qbittorrent re-auth: %w", err)
		}

		req2, err := http.NewRequestWithContext(ctx, method, q.baseURL+path, bodyReader)
		if err != nil {
			return nil, err
		}
		if body != nil {
			req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		}
		req2.AddCookie(q.sid)
		return q.client.Do(req2)
	}

	return resp, nil
}

func (q *QBittorrentClient) Add(ctx context.Context, torrentURL string, category string) (string, error) {
	if category == "" {
		category = "umm"
	}

	form := url.Values{}
	form.Set("urls", torrentURL)
	form.Set("category", category)

	resp, err := q.doAuthenticated(ctx, http.MethodPost, "/api/v2/torrents/add", form)
	if err != nil {
		return "", fmt.Errorf("qbittorrent add: %w", err)
	}
	resp.Body.Close()

	resp, err = q.doAuthenticated(ctx, http.MethodGet,
		fmt.Sprintf("/api/v2/torrents/info?sort=added_on&reverse=true&category=%s", url.QueryEscape(category)), nil)
	if err != nil {
		return "", fmt.Errorf("qbittorrent add verify: %w", err)
	}
	defer resp.Body.Close()

	var torrents []qbTorrentInfo
	if err := json.NewDecoder(resp.Body).Decode(&torrents); err != nil {
		return "", fmt.Errorf("qbittorrent add decode: %w", err)
	}

	for _, t := range torrents {
		if t.MagnetURI == torrentURL || strings.Contains(t.Tracker, torrentURL) {
			return t.Hash, nil
		}
	}

	if len(torrents) > 0 {
		return torrents[0].Hash, nil
	}

	return "", fmt.Errorf("qbittorrent add: torrent not found after adding")
}

func (q *QBittorrentClient) GetProgress(ctx context.Context, hash string) (*DownloadProgress, error) {
	resp, err := q.doAuthenticated(ctx, http.MethodGet,
		fmt.Sprintf("/api/v2/torrents/info?hashes=%s", url.QueryEscape(hash)), nil)
	if err != nil {
		return nil, fmt.Errorf("qbittorrent progress: %w", err)
	}
	defer resp.Body.Close()

	var torrents []qbTorrentInfo
	if err := json.NewDecoder(resp.Body).Decode(&torrents); err != nil {
		return nil, fmt.Errorf("qbittorrent progress decode: %w", err)
	}

	if len(torrents) == 0 {
		return nil, fmt.Errorf("qbittorrent: torrent %s not found", hash)
	}

	t := torrents[0]
	return &DownloadProgress{
		ID:       t.Hash,
		Name:     t.Name,
		Status:   qbStateToStatus(t.State),
		Progress: t.Progress * 100,
		Speed:    t.DLSpeed,
		ETA:      t.ETA,
		Size:     t.Size,
	}, nil
}

func (q *QBittorrentClient) GetCompleted(ctx context.Context) ([]CompletedDownload, error) {
	resp, err := q.doAuthenticated(ctx, http.MethodGet,
		"/api/v2/torrents/info?filter=completed", nil)
	if err != nil {
		return nil, fmt.Errorf("qbittorrent completed: %w", err)
	}
	defer resp.Body.Close()

	var torrents []qbTorrentInfo
	if err := json.NewDecoder(resp.Body).Decode(&torrents); err != nil {
		return nil, fmt.Errorf("qbittorrent completed decode: %w", err)
	}

	var completed []CompletedDownload
	for _, t := range torrents {
		completed = append(completed, CompletedDownload{
			ID:         t.Hash,
			Name:       t.Name,
			OutputPath: t.ContentPath,
			Size:       t.Size,
			Status:     qbStateToStatus(t.State),
		})
	}

	return completed, nil
}

func (q *QBittorrentClient) Remove(ctx context.Context, hash string) error {
	form := url.Values{}
	form.Set("hashes", hash)
	form.Set("deleteFiles", "true")

	resp, err := q.doAuthenticated(ctx, http.MethodPost, "/api/v2/torrents/delete", form)
	if err != nil {
		return fmt.Errorf("qbittorrent remove: %w", err)
	}
	resp.Body.Close()

	return nil
}

func (q *QBittorrentClient) Pause(ctx context.Context, hash string) error {
	form := url.Values{}
	form.Set("hashes", hash)

	resp, err := q.doAuthenticated(ctx, http.MethodPost, "/api/v2/torrents/pause", form)
	if err != nil {
		return fmt.Errorf("qbittorrent pause: %w", err)
	}
	resp.Body.Close()

	return nil
}

func (q *QBittorrentClient) Resume(ctx context.Context, hash string) error {
	form := url.Values{}
	form.Set("hashes", hash)

	resp, err := q.doAuthenticated(ctx, http.MethodPost, "/api/v2/torrents/resume", form)
	if err != nil {
		return fmt.Errorf("qbittorrent resume: %w", err)
	}
	resp.Body.Close()

	return nil
}

func qbStateToStatus(state string) string {
	switch state {
	case "downloading", "stalledDL", "forcedDL", "metaDL", "forcedMetaDL":
		return "downloading"
	case "uploading", "stalledUP", "forcedUP":
		return "completed"
	case "pausedDL", "pausedUP":
		return "paused"
	case "queuedDL", "queuedUP":
		return "queued"
	case "checkingDL", "checkingUP", "moving":
		return "checking"
	case "error", "missingFiles", "unknown":
		return "error"
	default:
		return "unknown"
	}
}

type qbTorrentInfo struct {
	Hash        string  `json:"hash"`
	Name        string  `json:"name"`
	State       string  `json:"state"`
	Progress    float64 `json:"progress"`
	DLSpeed     int64   `json:"dlspeed"`
	ETA         int     `json:"eta"`
	Size        int64   `json:"size"`
	ContentPath string  `json:"content_path"`
	AddedOn     int64   `json:"added_on"`
	Tracker     string  `json:"tracker"`
	MagnetURI   string  `json:"magnet_uri"`
}

var _ = strconv.Atoi

var _ io.Reader = nil
