// Package igw — REST-клиент Интеграционного шлюза (ИШ) НРД.
// Тело пакета передаётся base64 в JSON; ИШ сам подписывает и
// упаковывает в ZIP-пакет ЭДО по правилам НРД.
package igw

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

// Client — REST-клиент ИШ НРД.
type Client struct {
	baseURL    string
	httpClient *http.Client
	retryMax   int
	retryWait  time.Duration
}

// Option настраивает Client.
type Option func(*Client)

// WithHTTPClient заменяет стандартный http.Client (для тестов и
// фиксации таймаутов).
func WithHTTPClient(c *http.Client) Option {
	return func(cl *Client) { cl.httpClient = c }
}

// WithRetry задаёт количество ретраев и базовое ожидание (линейный backoff).
func WithRetry(max int, wait time.Duration) Option {
	return func(cl *Client) { cl.retryMax = max; cl.retryWait = wait }
}

// NewClient собирает клиента к ИШ по URL.
func NewClient(baseURL string, opts ...Option) *Client {
	c := &Client{
		baseURL:    strings.TrimRight(baseURL, "/"),
		httpClient: &http.Client{Timeout: 30 * time.Second},
		retryMax:   3,
		retryWait:  time.Second,
	}
	for _, o := range opts {
		o(c)
	}
	return c
}

// SendPackage отправляет пакет в указанный канал ЭДО. Возвращает
// идентификатор пакета, присвоенный ИШ.
func (c *Client) SendPackage(ctx context.Context, channel, packageType string, body []byte) (string, error) {
	if channel == "" {
		return "", errors.New("igw: channel пустой")
	}
	if packageType == "" {
		return "", errors.New("igw: packageType пустой")
	}
	payload := struct {
		PackageType string `json:"package_type"`
		Body        string `json:"body"`
	}{
		PackageType: packageType,
		Body:        base64.StdEncoding.EncodeToString(body),
	}
	raw, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("igw: marshal payload: %w", err)
	}
	path := fmt.Sprintf("/api/package/%s/file", url.PathEscape(channel))
	resp, err := c.doRetry(ctx, http.MethodPost, path, bytes.NewReader(raw), "application/json")
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	var out struct {
		PackageID string `json:"package_id"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return "", fmt.Errorf("igw: decode SendPackage response: %w", err)
	}
	if out.PackageID == "" {
		return "", errors.New("igw: пустой package_id в ответе ИШ")
	}
	return out.PackageID, nil
}

// Status — состояние пакета у ИШ.
type Status struct {
	PackageID string    `json:"package_id"`
	State     string    `json:"state"`
	UpdatedAt time.Time `json:"updated_at"`
	ErrorCode string    `json:"error_code,omitempty"`
	ErrorText string    `json:"error_text,omitempty"`
}

// GetStatus возвращает текущее состояние пакета по идентификатору.
func (c *Client) GetStatus(ctx context.Context, packageID string) (Status, error) {
	if packageID == "" {
		return Status{}, errors.New("igw: packageID пустой")
	}
	path := "/api/package/status/" + url.PathEscape(packageID)
	resp, err := c.doRetry(ctx, http.MethodGet, path, nil, "")
	if err != nil {
		return Status{}, err
	}
	defer resp.Body.Close()
	var s Status
	if err := json.NewDecoder(resp.Body).Decode(&s); err != nil {
		return Status{}, fmt.Errorf("igw: decode Status: %w", err)
	}
	return s, nil
}

// Package — описание входящего пакета.
type Package struct {
	PackageID   string    `json:"package_id"`
	PackageType string    `json:"package_type"`
	Channel     string    `json:"channel"`
	ReceivedAt  time.Time `json:"received_at"`
	Body        string    `json:"body,omitempty"` // base64
}

// DecodeBody возвращает декодированное содержимое пакета.
func (p Package) DecodeBody() ([]byte, error) {
	if p.Body == "" {
		return nil, nil
	}
	return base64.StdEncoding.DecodeString(p.Body)
}

// ListIncoming возвращает список входящих пакетов по фильтрам.
func (c *Client) ListIncoming(ctx context.Context, channel string, since time.Time, packageType string) ([]Package, error) {
	q := url.Values{}
	if channel != "" {
		q.Set("channel", channel)
	}
	if !since.IsZero() {
		q.Set("date", since.UTC().Format(time.RFC3339))
	}
	if packageType != "" {
		q.Set("type", packageType)
	}
	path := "/api/package?" + q.Encode()
	resp, err := c.doRetry(ctx, http.MethodGet, path, nil, "")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	var out struct {
		Items []Package `json:"items"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, fmt.Errorf("igw: decode ListIncoming: %w", err)
	}
	return out.Items, nil
}

// doRetry выполняет HTTP-запрос с ретраями на сетевые ошибки и 5xx.
func (c *Client) doRetry(ctx context.Context, method, path string, body io.Reader, contentType string) (*http.Response, error) {
	var lastErr error
	var bodyBytes []byte
	if body != nil {
		b, err := io.ReadAll(body)
		if err != nil {
			return nil, err
		}
		bodyBytes = b
	}
	for attempt := 0; attempt <= c.retryMax; attempt++ {
		req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(bodyBytes))
		if err != nil {
			return nil, err
		}
		if contentType != "" {
			req.Header.Set("Content-Type", contentType)
		}
		req.Header.Set("Accept", "application/json")
		resp, err := c.httpClient.Do(req)
		switch {
		case err != nil:
			lastErr = err
		case resp.StatusCode >= 500:
			_ = resp.Body.Close()
			lastErr = fmt.Errorf("igw: HTTP %d", resp.StatusCode)
		case resp.StatusCode >= 400:
			defer resp.Body.Close()
			b, _ := io.ReadAll(resp.Body)
			return nil, fmt.Errorf("igw: HTTP %d: %s", resp.StatusCode, string(b))
		default:
			return resp, nil
		}
		if attempt < c.retryMax {
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			case <-time.After(c.retryWait * time.Duration(attempt+1)):
			}
		}
	}
	return nil, fmt.Errorf("igw: исчерпаны ретраи: %w", lastErr)
}
