package main

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

const (
	defaultBaseURL = "https://api.preseem.com"
	apiPrefix      = "/model/v1"
	minLimit       = 100
	maxLimit       = 1000
)

type Client struct {
	BaseURL string
	Key     string
	HTTP    *http.Client
}

func NewClient() (*Client, error) {
	key := os.Getenv("PRESEEM_KEY")
	if key == "" {
		return nil, errors.New("PRESEEM_KEY env var is not set")
	}
	base := os.Getenv("PRESEEM_URL")
	if base == "" {
		base = defaultBaseURL
	}
	return &Client{
		BaseURL: strings.TrimRight(base, "/"),
		Key:     key,
		HTTP:    &http.Client{Timeout: 60 * time.Second},
	}, nil
}

// get performs GET against an API path (must start with /model/v1/...).
// The path should already be URL-encoded — callers building paths with
// user-supplied IDs must use pathID().
//
// If raw is non-nil it receives the raw response body. If out is non-nil
// it is JSON-decoded.
func (c *Client) get(path string, out any, raw *[]byte) error {
	u := c.BaseURL + path
	req, err := http.NewRequest(http.MethodGet, u, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Basic "+basicAuth(c.Key))
	req.Header.Set("Accept", "application/json")
	resp, err := c.HTTP.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 400 {
		return fmt.Errorf("GET %s: %s: %s", u, resp.Status, strings.TrimSpace(truncate(string(data), 400)))
	}
	if raw != nil {
		*raw = data
	}
	if out != nil && len(data) > 0 {
		if err := json.Unmarshal(data, out); err != nil {
			return fmt.Errorf("decode %s: %w (body: %s)", u, err, truncate(string(data), 200))
		}
	}
	return nil
}

// del performs DELETE against an API path.
func (c *Client) del(path string) error {
	u := c.BaseURL + path
	req, err := http.NewRequest(http.MethodDelete, u, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Basic "+basicAuth(c.Key))
	resp, err := c.HTTP.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		data, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("DELETE %s: %s: %s", u, resp.Status, strings.TrimSpace(truncate(string(data), 400)))
	}
	return nil
}

// listPaged fetches a single page of a list endpoint. base is e.g.
// "/accounts" (no /model/v1 prefix, no leading query). page/limit are
// added as query params; if limit<=0 the server default is used.
func (c *Client) listPaged(base string, page, limit int, out any, raw *[]byte) error {
	q := url.Values{}
	if page > 0 {
		q.Set("page", strconv.Itoa(page))
	}
	if limit > 0 {
		q.Set("limit", strconv.Itoa(limit))
	}
	path := apiPrefix + base
	if s := q.Encode(); s != "" {
		path += "?" + s
	}
	return c.get(path, out, raw)
}

// listAll auto-paginates a list endpoint into one merged result. It
// repeatedly invokes fetchPage(page) until the returned paginator says
// we've passed page_count. The collector appends each page's data
// items; the final paginator is returned for synthetic display.
func (c *Client) listAll(base string, limit int, decode func([]byte) ([]json.RawMessage, Paginator, error)) ([]json.RawMessage, Paginator, error) {
	var all []json.RawMessage
	var last Paginator
	page := 1
	for {
		var raw []byte
		if err := c.listPaged(base, page, limit, nil, &raw); err != nil {
			return nil, Paginator{}, err
		}
		items, p, err := decode(raw)
		if err != nil {
			return nil, Paginator{}, fmt.Errorf("decode page %d: %w", page, err)
		}
		all = append(all, items...)
		last = p
		// Stop if server reports no further pages, or returned empty.
		if len(items) == 0 || p.PageCount == 0 || page >= p.PageCount {
			break
		}
		page++
	}
	// Synthetic paginator for display: collapsed to one logical page.
	last.Page = 1
	last.PageCount = 1
	last.Limit = len(all)
	last.TotalCount = len(all)
	return all, last, nil
}

func basicAuth(key string) string {
	// Username = key, password empty, per Preseem spec.
	return base64.StdEncoding.EncodeToString([]byte(key + ":"))
}

// pathID URL-encodes an ID for inclusion in a path segment. Per spec,
// IDs may contain '/', which must become '%2F'.
func pathID(id string) string {
	return url.PathEscape(id)
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "..."
}

func validateLimit(limit int) error {
	if limit == 0 {
		return nil
	}
	if limit < minLimit || limit > maxLimit {
		return fmt.Errorf("--limit must be between %d and %d (got %d)", minLimit, maxLimit, limit)
	}
	return nil
}
