// Package sonarr provides the small read-only slice of Sonarr used by the home screen.
package sonarr

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

type Client struct {
	baseURL string
	apiKey  string
	http    *http.Client
}

type Image struct {
	CoverType string `json:"coverType"`
	URL       string `json:"url"`
	RemoteURL string `json:"remoteUrl"`
}

type Series struct {
	ID               int        `json:"id"`
	TVDBID           int        `json:"tvdbId"`
	Title            string     `json:"title"`
	TitleSlug        string     `json:"titleSlug"`
	Overview         string     `json:"overview"`
	Year             int        `json:"year"`
	Network          string     `json:"network"`
	Genres           []string   `json:"genres"`
	Images           []Image    `json:"images"`
	RootFolderPath   string     `json:"rootFolderPath,omitempty"`
	QualityProfileID int        `json:"qualityProfileId,omitempty"`
	Monitored        bool       `json:"monitored"`
	SeasonFolder     bool       `json:"seasonFolder"`
	Seasons          []Season   `json:"seasons"`
	Status           string     `json:"status"`
	NextAiring       *time.Time `json:"nextAiring"`
}

// Series returns Sonarr's current catalogue, including lifecycle and next-airing data.
func (c *Client) Series(ctx context.Context) ([]Series, error) {
	var series []Series
	if err := c.get(ctx, "/api/v3/series", &series); err != nil {
		return nil, err
	}
	return series, nil
}

// Episodes returns Sonarr's complete episode list for one series. Unlike the calendar,
// this includes future episodes, which is what makes finale detection trustworthy rather
// than mistaking the newest downloaded episode for the end of a season.
func (c *Client) Episodes(ctx context.Context, seriesID int) ([]Episode, error) {
	if seriesID <= 0 {
		return nil, fmt.Errorf("sonarr: invalid series id")
	}
	req, err := c.request(ctx, "/api/v3/episode", url.Values{
		"seriesId":      {strconv.Itoa(seriesID)},
		"includeSeries": {"true"},
	})
	if err != nil {
		return nil, err
	}
	var episodes []Episode
	if err := c.do(req, &episodes); err != nil {
		return nil, err
	}
	return episodes, nil
}

type Season struct {
	SeasonNumber int  `json:"seasonNumber"`
	Monitored    bool `json:"monitored"`
}

type RootFolder struct {
	Path string `json:"path"`
}

type QualityProfile struct {
	ID int `json:"id"`
}

type EpisodeFile struct {
	DateAdded *time.Time `json:"dateAdded"`
}

type Episode struct {
	ID            int `json:"id"`
	SeriesID      int `json:"seriesId"`
	SeasonNumber  int `json:"seasonNumber"`
	EpisodeNumber int `json:"episodeNumber"`
	// Sonarr names confirmed endings (for example "seasonFinale" and
	// "seriesFinale"). Empty means it has made no finale claim.
	FinaleType  string       `json:"finaleType"`
	Title       string       `json:"title"`
	Overview    string       `json:"overview"`
	AirDateUTC  *time.Time   `json:"airDateUtc"`
	Runtime     int          `json:"runtime"`
	HasFile     bool         `json:"hasFile"`
	Monitored   bool         `json:"monitored"`
	Grabbed     bool         `json:"grabbed"`
	Series      Series       `json:"series"`
	EpisodeFile *EpisodeFile `json:"episodeFile"`
}

type APIError struct {
	StatusCode int
	Body       string
}

func (e *APIError) Error() string {
	return fmt.Sprintf("sonarr: status %d: %s", e.StatusCode, e.Body)
}

func New(baseURL, apiKey string, timeout time.Duration) *Client {
	return &Client{
		baseURL: strings.TrimRight(baseURL, "/"),
		apiKey:  apiKey,
		http: &http.Client{
			Timeout: timeout,
			Transport: &http.Transport{
				MaxIdleConns:        20,
				MaxIdleConnsPerHost: 10,
				IdleConnTimeout:     90 * time.Second,
			},
		},
	}
}

// Calendar returns episodes in [start, end), including series artwork and imported-file
// details so Memby can distinguish upcoming, downloading and already-added episodes.
func (c *Client) Calendar(ctx context.Context, start, end time.Time) ([]Episode, error) {
	params := url.Values{
		"start":                {start.UTC().Format(time.RFC3339Nano)},
		"end":                  {end.UTC().Format(time.RFC3339Nano)},
		"unmonitored":          {"true"},
		"includeSeries":        {"true"},
		"includeEpisodeFile":   {"true"},
		"includeEpisodeImages": {"true"},
	}
	req, err := c.request(ctx, "/api/v3/calendar", params)
	if err != nil {
		return nil, err
	}
	var episodes []Episode
	if err := c.do(req, &episodes); err != nil {
		return nil, err
	}
	return episodes, nil
}

func (c *Client) Lookup(ctx context.Context, term string) ([]Series, error) {
	req, err := c.request(ctx, "/api/v3/series/lookup", url.Values{"term": {term}})
	if err != nil {
		return nil, err
	}
	var series []Series
	if err := c.do(req, &series); err != nil {
		return nil, err
	}
	return series, nil
}

// AddUnmonitored adds a series without monitoring it or starting an episode search.
func (c *Client) AddUnmonitored(ctx context.Context, series Series) (Series, error) {
	var roots []RootFolder
	if err := c.get(ctx, "/api/v3/rootfolder", &roots); err != nil {
		return Series{}, err
	}
	var profiles []QualityProfile
	if err := c.get(ctx, "/api/v3/qualityprofile", &profiles); err != nil {
		return Series{}, err
	}
	if len(roots) == 0 || len(profiles) == 0 {
		return Series{}, fmt.Errorf("sonarr: no root folder or quality profile configured")
	}
	series.ID = 0
	series.RootFolderPath = roots[0].Path
	series.QualityProfileID = profiles[0].ID
	series.Monitored = false
	series.SeasonFolder = true
	for i := range series.Seasons {
		series.Seasons[i].Monitored = false
	}
	body := struct {
		Series
		AddOptions map[string]bool `json:"addOptions"`
	}{Series: series, AddOptions: map[string]bool{"searchForMissingEpisodes": false}}
	var added Series
	if err := c.post(ctx, "/api/v3/series", body, &added); err != nil {
		return Series{}, err
	}
	return added, nil
}

// MediaCover fetches a series poster or fanart without exposing the Sonarr API key.
func (c *Client) MediaCover(ctx context.Context, seriesID int, coverType string) (*http.Response, error) {
	if seriesID <= 0 || (coverType != "poster" && coverType != "fanart") {
		return nil, fmt.Errorf("sonarr: invalid media cover")
	}
	path := "/MediaCover/" + strconv.Itoa(seriesID) + "/" + coverType + ".jpg"
	req, err := c.request(ctx, path, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Accept", "image/*")
	resp, err := c.http.Do(req)
	if err != nil {
		return nil, fmt.Errorf("sonarr: %w", err)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		defer resp.Body.Close()
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
		return nil, &APIError{StatusCode: resp.StatusCode, Body: strings.TrimSpace(string(body))}
	}
	return resp, nil
}

func (c *Client) request(ctx context.Context, path string, params url.Values) (*http.Request, error) {
	endpoint := c.baseURL + path
	if len(params) > 0 {
		endpoint += "?" + params.Encode()
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-Api-Key", c.apiKey)
	req.Header.Set("Accept", "application/json")
	return req, nil
}

func (c *Client) get(ctx context.Context, path string, out any) error {
	req, err := c.request(ctx, path, nil)
	if err != nil {
		return err
	}
	return c.do(req, out)
}

func (c *Client) post(ctx context.Context, path string, body, out any) error {
	raw, err := json.Marshal(body)
	if err != nil {
		return fmt.Errorf("sonarr: encode request: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, strings.NewReader(string(raw)))
	if err != nil {
		return err
	}
	req.Header.Set("X-Api-Key", c.apiKey)
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	return c.do(req, out)
}

func (c *Client) do(req *http.Request, out any) error {
	resp, err := c.http.Do(req)
	if err != nil {
		return fmt.Errorf("sonarr: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
		return &APIError{StatusCode: resp.StatusCode, Body: strings.TrimSpace(string(body))}
	}
	if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
		return fmt.Errorf("sonarr: decode response: %w", err)
	}
	return nil
}
