package release

import (
	"context"
	"crypto/ed25519"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// Client — клиент артефактории: скачивает подписанный манифест и артефакты,
// проверяет подпись зашитым публичным ключом и sha256 каждого файла.
type Client struct {
	BaseURL string            // напр. https://updates.example.com
	Channel string            // "stable" | "beta"
	Pub     ed25519.PublicKey // публичный ключ издателя (зашит в bj-server)
	HTTP    *http.Client
}

// NewClient собирает клиент. pubB64 — публичный ключ в base64 (зашитый).
func NewClient(baseURL, channel, pubB64 string) (*Client, error) {
	pub, err := ParsePublicKey(pubB64)
	if err != nil {
		return nil, fmt.Errorf("release: публичный ключ: %w", err)
	}
	return &Client{
		BaseURL: strings.TrimRight(baseURL, "/"),
		Channel: channel,
		Pub:     pub,
		HTTP:    &http.Client{Timeout: 60 * time.Second},
	}, nil
}

// FetchManifest скачивает /v1/<channel>/manifest.json и проверяет подпись.
// Возвращает доверенный Manifest (или ошибку, если подпись не сошлась).
func (c *Client) FetchManifest(ctx context.Context) (*Manifest, error) {
	url := fmt.Sprintf("%s/v1/%s/manifest.json", c.BaseURL, c.Channel)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.HTTP.Do(req)
	if err != nil {
		return nil, fmt.Errorf("release: запрос манифеста: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("release: манифест HTTP %d", resp.StatusCode)
	}
	var sm SignedManifest
	if err := json.NewDecoder(resp.Body).Decode(&sm); err != nil {
		return nil, fmt.Errorf("release: разбор манифеста: %w", err)
	}
	return Verify(&sm, c.Pub) // здесь проверяется подпись
}

// DownloadArtifact скачивает артефакт в destDir, проверяет sha256/размер по
// манифесту, выставляет +x при необходимости. Возвращает путь к файлу.
// Скачивает во временный файл и переименовывает атомарно.
func (c *Client) DownloadArtifact(ctx context.Context, a Artifact, destDir string) (string, error) {
	url := fmt.Sprintf("%s/v1/%s/files/%s", c.BaseURL, c.Channel, a.File)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return "", err
	}
	resp, err := c.HTTP.Do(req)
	if err != nil {
		return "", fmt.Errorf("release: скачивание %s: %w", a.Name, err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("release: %s HTTP %d", a.Name, resp.StatusCode)
	}

	tmp, err := os.CreateTemp(destDir, "."+a.File+".dl-*")
	if err != nil {
		return "", err
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName) // если что-то пойдёт не так — подчистим

	if _, err := io.Copy(tmp, resp.Body); err != nil {
		tmp.Close()
		return "", err
	}
	if err := tmp.Close(); err != nil {
		return "", err
	}
	// Проверка целостности по манифесту (подпись манифеста уже проверена).
	if err := VerifyArtifact(tmpName, a); err != nil {
		return "", err
	}
	mode := os.FileMode(0o644)
	if a.Exec {
		mode = 0o755
	}
	if err := os.Chmod(tmpName, mode); err != nil {
		return "", err
	}
	final := filepath.Join(destDir, a.File)
	if err := os.Rename(tmpName, final); err != nil { // атомарно в пределах destDir
		return "", err
	}
	return final, nil
}
