// SPDX-License-Identifier: GPL-3.0-or-later

package client

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

// UpdateState mirrors the daemon's updater.State.
type UpdateState struct {
	CurrentVersion  string    `json:"current_version"`
	LatestVersion   string    `json:"latest_version,omitempty"`
	UpdateAvailable bool      `json:"update_available"`
	AutoUpdate      bool      `json:"auto_update"`
	Channel         string    `json:"channel"`
	LastChecked     time.Time `json:"last_checked,omitempty"`
	LastError       string    `json:"last_error,omitempty"`
	PendingPath     string    `json:"pending_path,omitempty"`
	ReleaseNotesURL string    `json:"release_notes_url,omitempty"`
}

// UpdateConfig is the persisted update settings exposed by the daemon.
type UpdateConfig struct {
	AutoUpdate bool   `json:"auto_update"`
	Channel    string `json:"channel"`
}

// UpdateStatus fetches the daemon's current updater state.
func (c *Client) UpdateStatus(ctx context.Context) (*UpdateState, error) {
	var s UpdateState
	if err := c.GetJSON(ctx, pathUpdatesStatus, &s); err != nil {
		return nil, err
	}
	return &s, nil
}

// CheckForUpdates asks the daemon to refresh the manifest. If install is
// true and the daemon detects a newer version, the backend daemon also
// downloads and applies its own update.
func (c *Client) CheckForUpdates(ctx context.Context, install bool) (*UpdateState, error) {
	path := pathUpdatesCheck
	if install {
		path = pathUpdatesCheck + "?install=true"
	}
	var s UpdateState
	if err := c.PostJSON(ctx, path, nil, &s); err != nil {
		return nil, err
	}
	return &s, nil
}

// GetUpdateConfig returns the persisted update settings.
func (c *Client) GetUpdateConfig(ctx context.Context) (*UpdateConfig, error) {
	var cfg UpdateConfig
	if err := c.GetJSON(ctx, pathUpdatesConfig, &cfg); err != nil {
		return nil, err
	}
	return &cfg, nil
}

// SetAutoUpdate toggles auto-update on or off via the daemon API.
func (c *Client) SetAutoUpdate(ctx context.Context, enabled bool) (*UpdateConfig, error) {
	body := map[string]any{"auto_update": enabled}
	var cfg UpdateConfig
	if err := c.PutJSON(ctx, pathUpdatesConfig, body, &cfg); err != nil {
		return nil, err
	}
	return &cfg, nil
}

// FetchUpdateBinary asks the daemon to download a verified component
// binary through Tor and stream it back over localhost. The caller is
// responsible for writing the bytes to disk and handling atomic
// replacement.
//
// The returned headers expose the manifest's SHA256 and size so the
// caller can sanity-check the streamed bytes; the daemon has already
// verified them before the body left its memory.
func (c *Client) FetchUpdateBinary(ctx context.Context, component string) (data []byte, sha256 string, size int64, err error) {
	resp, err := c.Get(ctx, pathUpdatesBinary+"/"+component)
	if err != nil {
		return nil, "", 0, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, "", 0, fmt.Errorf("daemon returned %d fetching %s", resp.StatusCode, component)
	}
	sha256 = resp.Header.Get("X-Artifact-SHA256")
	if s := resp.Header.Get("X-Artifact-Size"); s != "" {
		n, _ := strconv.ParseInt(s, 10, 64)
		size = n
	}
	data, err = io.ReadAll(resp.Body)
	if err != nil {
		return nil, "", 0, err
	}
	if size > 0 && int64(len(data)) != size {
		return nil, "", 0, errors.New("partial response: byte count does not match X-Artifact-Size")
	}
	return data, sha256, size, nil
}

// PutJSON performs a PUT request with a JSON body and decodes the JSON
// response. Mirrors PostJSON; lives here because PUT is otherwise unused
// in the CLI.
func (c *Client) PutJSON(ctx context.Context, path string, body, result any) error {
	data, err := json.Marshal(body)
	if err != nil {
		return fmt.Errorf("encoding body: %w", err)
	}
	resp, err := c.Put(ctx, path, &jsonReader{data: data})
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return c.parseError(resp)
	}
	if result == nil {
		return nil
	}
	if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
		return fmt.Errorf("decoding response: %w", err)
	}
	return nil
}
