// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package lunatask

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
)

// API error types for typed error handling
var (
	// ErrBadRequest indicates invalid, malformed, or missing parameters (400)
	ErrBadRequest = errors.New("bad request")
	// ErrUnauthorized indicates missing, wrong, or revoked access token (401)
	ErrUnauthorized = errors.New("unauthorized")
	// ErrPaymentRequired indicates a subscription is required (402)
	ErrPaymentRequired = errors.New("subscription required")
	// ErrNotFound indicates the specified entity could not be found (404)
	ErrNotFound = errors.New("not found")
	// ErrUnprocessableEntity indicates the provided entity is not valid (422)
	ErrUnprocessableEntity = errors.New("unprocessable entity")
	// ErrServerError indicates an internal server error (500)
	ErrServerError = errors.New("server error")
	// ErrServiceUnavailable indicates temporary maintenance (503)
	ErrServiceUnavailable = errors.New("service unavailable")
	// ErrTimeout indicates request timed out (524)
	ErrTimeout = errors.New("request timed out")
)

// APIError wraps an API error with status code and response body
type APIError struct {
	StatusCode int
	Body       string
	Err        error
}

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

func (e *APIError) Unwrap() error {
	return e.Err
}

// newAPIError creates an APIError from an HTTP status code
func newAPIError(statusCode int, body string) *APIError {
	var err error
	switch statusCode {
	case http.StatusBadRequest:
		err = ErrBadRequest
	case http.StatusUnauthorized:
		err = ErrUnauthorized
	case http.StatusPaymentRequired:
		err = ErrPaymentRequired
	case http.StatusNotFound:
		err = ErrNotFound
	case http.StatusUnprocessableEntity:
		err = ErrUnprocessableEntity
	case http.StatusInternalServerError:
		err = ErrServerError
	case http.StatusServiceUnavailable:
		err = ErrServiceUnavailable
	case 524:
		err = ErrTimeout
	default:
		err = fmt.Errorf("unexpected status %d", statusCode)
	}
	return &APIError{StatusCode: statusCode, Body: body, Err: err}
}

// Client handles communication with the Lunatask API
type Client struct {
	AccessToken string
	BaseURL     string
	HTTPClient  *http.Client
}

// NewClient creates a new Lunatask API client
func NewClient(accessToken string) *Client {
	return &Client{
		AccessToken: accessToken,
		BaseURL:     "https://api.lunatask.app/v1",
		HTTPClient:  &http.Client{},
	}
}

// doRequest performs an HTTP request and handles common response processing
func (c *Client) doRequest(req *http.Request) ([]byte, error) {
	req.Header.Set("Authorization", "bearer "+c.AccessToken)

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to send HTTP request: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response body: %w", err)
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, newAPIError(resp.StatusCode, string(body))
	}

	return body, nil
}

// Ping verifies the access token is valid by calling the /ping endpoint.
// Returns nil on success, or an error (typically ErrUnauthorized) on failure.
func (c *Client) Ping(ctx context.Context) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/ping", nil)
	if err != nil {
		return fmt.Errorf("failed to create HTTP request: %w", err)
	}

	_, err = c.doRequest(req)
	return err
}
