// Package autobrr is the read-side autobrr REST API client. Phase 5 uses
// it to discover filter definitions, fetch recent release activity, and
// expose indexer connection status. Phase 6 may extend this for filter
// toggle / mutation paths.
//
// Auth: autobrr's API uses a static `X-API-Token` header. The token lives
// in the age-encrypted secrets store under `automation_tool:<id>:api_token`
// (matching the Phase 1 / Phase 3 credential convention).
package autobrr

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

// SecretsAccessor is the narrow interface autobrr needs from
// internal/secrets — abstracted for test ergonomics, matching the pattern
// used by internal/integrations/qbit.
type SecretsAccessor interface {
	Get(ctx context.Context, key string) ([]byte, error)
}

// APITokenSecretKey returns the canonical secrets-store key for an autobrr
// API token. Exposed so the operator-side helper script (and any future
// in-UI editor) agrees with the client.
func APITokenSecretKey(toolID string) string {
	return "automation_tool:" + toolID + ":api_token"
}

// Client is one autobrr instance. Construct one with New; reuse it across
// requests.
type Client struct {
	id       string
	baseURL  string
	http     *http.Client
	secrets  SecretsAccessor
}

// Config is the constructor input.
type Config struct {
	ID      string
	BaseURL string
	HTTP    *http.Client // optional; default has a 30s timeout
	Secrets SecretsAccessor
}

// New constructs a client. BaseURL must be the autobrr root (e.g.
// http://127.0.0.1:7474), no trailing slash required.
func New(cfg Config) (*Client, error) {
	if cfg.ID == "" || cfg.BaseURL == "" {
		return nil, errors.New("autobrr: id and base_url required")
	}
	if cfg.Secrets == nil {
		return nil, errors.New("autobrr: secrets store required")
	}
	hc := cfg.HTTP
	if hc == nil {
		hc = &http.Client{Timeout: 30 * time.Second}
	}
	return &Client{
		id:      cfg.ID,
		baseURL: strings.TrimRight(cfg.BaseURL, "/"),
		http:    hc,
		secrets: cfg.Secrets,
	}, nil
}

// ID returns the operator-facing id.
func (c *Client) ID() string { return c.id }

// Filters returns the configured filters.
func (c *Client) Filters(ctx context.Context) ([]Filter, error) {
	var out []Filter
	if err := c.getJSON(ctx, "/api/filters", &out); err != nil {
		return nil, err
	}
	return out, nil
}

// Filter returns one filter by id.
func (c *Client) Filter(ctx context.Context, id int) (Filter, error) {
	var out Filter
	if err := c.getJSON(ctx, "/api/filters/"+strconv.Itoa(id), &out); err != nil {
		return Filter{}, err
	}
	return out, nil
}

// RecentReleases returns the most recent release-history rows. limit is
// clamped to [1, 1000].
func (c *Client) RecentReleases(ctx context.Context, limit int) ([]Release, error) {
	if limit <= 0 {
		limit = 100
	}
	if limit > 1000 {
		limit = 1000
	}
	v := url.Values{}
	v.Set("limit", strconv.Itoa(limit))

	// autobrr's release endpoint pagination has varied across versions.
	// First try /api/release with a limit query; some versions wrap the
	// list in {"data":[...], "count":N}.
	var raw json.RawMessage
	if err := c.getJSON(ctx, "/api/release?"+v.Encode(), &raw); err != nil {
		return nil, err
	}
	var wrapped struct {
		Data []Release `json:"data"`
	}
	if err := json.Unmarshal(raw, &wrapped); err == nil && wrapped.Data != nil {
		return wrapped.Data, nil
	}
	var direct []Release
	if err := json.Unmarshal(raw, &direct); err == nil {
		return direct, nil
	}
	return nil, fmt.Errorf("autobrr: unexpected release shape")
}

// IndexerStatuses returns the connection state of each configured indexer.
func (c *Client) IndexerStatuses(ctx context.Context) ([]IndexerStatus, error) {
	var out []IndexerStatus
	if err := c.getJSON(ctx, "/api/indexer/status", &out); err != nil {
		// Older autobrr versions don't expose this endpoint; tolerate.
		return []IndexerStatus{}, nil
	}
	return out, nil
}

// Health is a cheap reachability check.
func (c *Client) Health(ctx context.Context) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/healthz/liveness", nil)
	if err != nil {
		return err
	}
	resp, err := c.http.Do(req)
	if err != nil {
		return err
	}
	_ = resp.Body.Close()
	if resp.StatusCode == http.StatusOK {
		return nil
	}
	// /healthz isn't universal across versions; treat a 401/403 as "service
	// is up but rejecting unauthenticated probes" — also OK for health.
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return nil
	}
	return fmt.Errorf("autobrr: health: status %d", resp.StatusCode)
}

func (c *Client) getJSON(ctx context.Context, path string, out any) error {
	tok, err := c.secrets.Get(ctx, APITokenSecretKey(c.id))
	if err != nil {
		return fmt.Errorf("autobrr: missing api token for %s: %w", c.id, err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
	if err != nil {
		return err
	}
	req.Header.Set("X-API-Token", strings.TrimSpace(string(tok)))
	req.Header.Set("Accept", "application/json")
	resp, err := c.http.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		return fmt.Errorf("autobrr: GET %s: status %d: %s", path, resp.StatusCode, body)
	}
	return json.NewDecoder(io.LimitReader(resp.Body, 32<<20)).Decode(out)
}
