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

package newznab

import (
	"bytes"
	"context"
	"encoding/xml"
	"io"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"time"

	"github.com/autobrr/autobrr/pkg/errors"
	"github.com/autobrr/autobrr/pkg/sharedhttp"
)

const DefaultTimeout = 60

type Client interface {
	GetFeed(ctx context.Context) (*Feed, error)
	GetCaps(ctx context.Context) (*Caps, error)
	Caps() *Caps
}

type client struct {
	http *http.Client

	Host   string
	ApiKey string

	UseBasicAuth bool
	BasicAuth    BasicAuth

	Capabilities *Caps

	Log *log.Logger
}

type BasicAuth struct {
	Username string
	Password string
}

type Config struct {
	Host    string
	ApiKey  string
	Timeout time.Duration

	UseBasicAuth bool
	BasicAuth    BasicAuth

	Log *log.Logger
}

type Capabilities struct {
	Search     Searching
	Categories Categories
}

func NewClient(config Config) Client {
	httpClient := &http.Client{
		Timeout:   time.Second * DefaultTimeout,
		Transport: sharedhttp.Transport,
	}

	if config.Timeout > 0 {
		httpClient.Timeout = time.Second * config.Timeout
	}

	c := &client{
		http:   httpClient,
		Host:   config.Host,
		ApiKey: config.ApiKey,
		Log:    log.New(io.Discard, "", log.LstdFlags),
	}

	if config.Log != nil {
		c.Log = config.Log
	}

	return c
}

func (c *client) get(ctx context.Context, endpoint string, queryParams map[string]string) (int, *Feed, error) {
	params := url.Values{}
	params.Set("t", "search")

	for k, v := range queryParams {
		params.Add(k, v)
	}

	if c.ApiKey != "" {
		params.Add("apikey", c.ApiKey)
	}

	u, err := url.Parse(c.Host)
	u.Path = strings.TrimSuffix(u.Path, "/")
	u.RawQuery = params.Encode()
	reqUrl := u.String()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, nil)
	if err != nil {
		return 0, nil, errors.Wrap(err, "could not build request")
	}

	if c.UseBasicAuth {
		req.SetBasicAuth(c.BasicAuth.Username, c.BasicAuth.Password)
	}

	resp, err := c.http.Do(req)
	if err != nil {
		return 0, nil, errors.Wrap(err, "could not make request. %+v", req)
	}

	defer resp.Body.Close()

	dump, err := httputil.DumpResponse(resp, true)
	if err != nil {
		return 0, nil, errors.Wrap(err, "could not dump response")
	}

	c.Log.Printf("newznab get feed response dump: %q", dump)

	var buf bytes.Buffer
	if _, err = io.Copy(&buf, resp.Body); err != nil {
		return resp.StatusCode, nil, errors.Wrap(err, "newznab.io.Copy")
	}

	var response Feed
	if err := xml.Unmarshal(buf.Bytes(), &response); err != nil {
		return resp.StatusCode, nil, errors.Wrap(err, "newznab: could not decode feed")
	}

	response.Raw = buf.String()

	return resp.StatusCode, &response, nil
}

func (c *client) getData(ctx context.Context, endpoint string, queryParams map[string]string) (*http.Response, error) {
	u, err := url.Parse(c.Host)
	if err != nil {
		return nil, errors.Wrap(err, "could not build request")
	}
	u.Path = strings.TrimSuffix(u.Path, "/")

	qp, err := url.ParseQuery(u.RawQuery)
	if err != nil {
		return nil, errors.Wrap(err, "could not build request")
	}

	if c.ApiKey != "" {
		qp.Add("apikey", c.ApiKey)
	}

	for k, v := range queryParams {
		if qp.Has("t") {
			continue
		}
		qp.Add(k, v)
	}

	u.RawQuery = qp.Encode()
	reqUrl := u.String()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, nil)
	if err != nil {
		return nil, errors.Wrap(err, "could not build request")
	}

	if c.UseBasicAuth {
		req.SetBasicAuth(c.BasicAuth.Username, c.BasicAuth.Password)
	}

	resp, err := c.http.Do(req)
	if err != nil {
		return resp, errors.Wrap(err, "could not make request. %+v", req)
	}

	return resp, nil
}

func (c *client) GetFeed(ctx context.Context) (*Feed, error) {

	p := map[string]string{"t": "search"}

	resp, err := c.getData(ctx, "", p)
	if err != nil {
		return nil, errors.Wrap(err, "could not get feed")
	}

	defer resp.Body.Close()

	dump, err := httputil.DumpResponse(resp, true)
	if err != nil {
		return nil, errors.Wrap(err, "could not dump response")
	}

	c.Log.Printf("newznab get feed response dump: %q", dump)

	if resp.StatusCode != http.StatusOK {
		return nil, errors.New("could not get feed")
	}

	var buf bytes.Buffer
	if _, err = io.Copy(&buf, resp.Body); err != nil {
		return nil, errors.Wrap(err, "newznab.io.Copy")
	}

	var response Feed
	if err := xml.Unmarshal(buf.Bytes(), &response); err != nil {
		return nil, errors.Wrap(err, "newznab: could not decode feed")
	}

	response.Raw = buf.String()

	if c.Capabilities != nil {
		for _, item := range response.Channel.Items {
			item.MapCustomCategoriesFromAttr(c.Capabilities.Categories.Categories)
		}
	} else {
		for _, item := range response.Channel.Items {
			item.MapCategoriesFromAttr()
		}
	}

	return &response, nil
}

func (c *client) GetFeedAndCaps(ctx context.Context) (*Feed, error) {
	if c.Capabilities == nil {
		status, caps, err := c.getCaps(ctx, "?t=caps", nil)
		if err != nil {
			return nil, errors.Wrap(err, "could not get caps for feed")
		}

		if status != http.StatusOK {
			return nil, errors.Wrap(err, "could not get caps for feed")
		}

		c.Capabilities = caps
	}

	p := map[string]string{"t": "search"}

	status, res, err := c.get(ctx, "", p)
	if err != nil {
		return nil, errors.Wrap(err, "could not get feed")
	}

	if status != http.StatusOK {
		return nil, errors.New("could not get feed")
	}

	for _, item := range res.Channel.Items {
		item.MapCustomCategoriesFromAttr(c.Capabilities.Categories.Categories)
	}

	return res, nil
}

func (c *client) getCaps(ctx context.Context, endpoint string, opts map[string]string) (int, *Caps, error) {
	params := url.Values{
		"t": {"caps"},
	}

	if c.ApiKey != "" {
		params.Add("apikey", c.ApiKey)
	}

	u, err := url.Parse(c.Host)
	u.Path = strings.TrimSuffix(u.Path, "/")
	u.RawQuery = params.Encode()
	reqUrl := u.String()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, nil)
	if err != nil {
		return 0, nil, errors.Wrap(err, "could not build request")
	}

	if c.UseBasicAuth {
		req.SetBasicAuth(c.BasicAuth.Username, c.BasicAuth.Password)
	}

	// Jackett only supports api key via url param while Prowlarr does that and via header
	//if c.ApiKey != "" {
	//	req.Header.Add("X-API-Key", c.ApiKey)
	//}

	resp, err := c.http.Do(req)
	if err != nil {
		return 0, nil, errors.Wrap(err, "could not make request. %+v", req)
	}

	defer resp.Body.Close()

	dump, err := httputil.DumpResponse(resp, true)
	if err != nil {
		return 0, nil, errors.Wrap(err, "could not dump response")
	}

	c.Log.Printf("newznab get caps response dump: %q", dump)

	if resp.StatusCode == http.StatusUnauthorized {
		return resp.StatusCode, nil, errors.New("unauthorized")
	} else if resp.StatusCode != http.StatusOK {
		return resp.StatusCode, nil, errors.New("bad status: %d", resp.StatusCode)
	}

	var buf bytes.Buffer
	if _, err = io.Copy(&buf, resp.Body); err != nil {
		return resp.StatusCode, nil, errors.Wrap(err, "newznab.io.Copy")
	}

	var response Caps
	if err := xml.Unmarshal(buf.Bytes(), &response); err != nil {
		return resp.StatusCode, nil, errors.Wrap(err, "newznab: could not decode feed")
	}

	return resp.StatusCode, &response, nil
}

func (c *client) GetCaps(ctx context.Context) (*Caps, error) {

	status, res, err := c.getCaps(ctx, "?t=caps", nil)
	if err != nil {
		return nil, errors.Wrap(err, "could not get caps for feed")
	}

	if status != http.StatusOK {
		return nil, errors.Wrap(err, "could not get caps for feed")
	}

	return res, nil
}

func (c *client) Caps() *Caps {
	return c.Capabilities
}

//func (c *client) Search(ctx context.Context, query string) ([]FeedItem, error) {
//	v := url.Values{}
//	v.Add("q", query)
//	params := v.Encode()
//
//	status, res, err := c.get(ctx, "&t=search&"+params, nil)
//	if err != nil {
//		return nil, errors.Wrap(err, "could not search feed")
//	}
//
//	if status != http.StatusOK {
//		return nil, errors.New("could not search feed")
//	}
//
//	return res.Channel.Items, nil
//}
