package client

import (
	"context"
	"encoding/json"
	"net/url"
	"strconv"
	"time"

	"github.com/keys-pub/keys"
	"github.com/keys-pub/keys-ext/http/api"
	"github.com/keys-pub/keys/dstore"
	"github.com/keys-pub/keys/tsutil"
	"github.com/pkg/errors"
	"github.com/vmihailenco/msgpack/v4"
)

// Vault events from the API, decrypted with vault API key.
type Vault struct {
	Events    []*VaultEvent
	Index     int64
	Truncated bool
}

// VaultEvent describes a vault event.
type VaultEvent struct {
	// Path for event.
	Path string `json:"path" msgpack:"p"`
	// Data ...
	Data []byte `json:"data" msgpack:"dat"`

	// RemoteIndex is set from the remote events API (untrusted).
	RemoteIndex int64 `json:"-" msgpack:"-"`
	// RemoteTimestamp is set from the remote events API (untrusted).
	RemoteTimestamp time.Time `json:"-" msgpack:"-"`

	// Deprecated fields, don't reuse these tag names.
	// Nonce to prevent replay.
	// Nonce []byte `msgpack:"n"`
	// Prev is a hash of the previous item.
	// Prev []byte `msgpack:"prv,omitempty"`
}

// VaultSend saves events to the vault API with a key.
// Events are encrypted with the key before saving.
func (c *Client) VaultSend(ctx context.Context, key *keys.EdX25519Key, events []*VaultEvent) error {
	path := dstore.Path("vault", key.ID())

	out := []*api.Data{}
	for _, event := range events {
		if !event.RemoteTimestamp.IsZero() {
			return errors.Errorf("remote timestamp should be omitted on send")
		}
		if event.RemoteIndex != 0 {
			return errors.Errorf("remote index should be omitted on send")
		}
		b, err := msgpack.Marshal(event)
		if err != nil {
			return err
		}
		out = append(out, &api.Data{
			Data: vaultEncrypt(b, key),
		})
	}

	// TODO: Support msgpack
	b, err := json.Marshal(out)
	if err != nil {
		return err
	}

	if _, err := c.req(ctx, request{Method: "POST", Path: path, Body: b, Key: key}); err != nil {
		return err
	}
	return nil
}

// VaultOptions options for Vault.
type VaultOptions struct {
	// Index to list to/from
	Index int64
	// Limit by
	Limit int
}

// VaultOption option.
type VaultOption func(o *VaultOptions)

// VaultIndex ...
func VaultIndex(index int64) VaultOption {
	return func(o *VaultOptions) {
		o.Index = index
	}
}

// VaultLimit ...
func VaultLimit(limit int) VaultOption {
	return func(o *VaultOptions) {
		o.Limit = limit
	}
}

func newVaultOptions(opts ...VaultOption) VaultOptions {
	var options VaultOptions
	for _, o := range opts {
		o(&options)
	}
	return options
}

// Vault events.
// Vault data is decrypted using the vault key before being returned.
// If truncated, there are more results if you call again with the new index.
func (c *Client) Vault(ctx context.Context, key *keys.EdX25519Key, opt ...VaultOption) (*Vault, error) {
	opts := newVaultOptions(opt...)
	path := dstore.Path("vault", key.ID())
	params := url.Values{}
	if opts.Index != 0 {
		params.Add("idx", strconv.FormatInt(opts.Index, 10))
	}
	if opts.Limit != 0 {
		// TODO: Support limit
		return nil, errors.Errorf("limit not currently supported")
	}

	resp, err := c.req(ctx, request{Method: "GET", Path: path, Params: params, Key: key})
	if err != nil {
		return nil, err
	}
	if resp == nil {
		return nil, nil
	}

	var out api.VaultResponse
	if err := json.Unmarshal(resp.Data, &out); err != nil {
		return nil, err
	}

	return vaultDecryptResponse(&out, key)
}

func vaultDecryptResponse(resp *api.VaultResponse, key *keys.EdX25519Key) (*Vault, error) {
	out := make([]*VaultEvent, 0, len(resp.Vault))
	for _, revent := range resp.Vault {
		decrypted, err := vaultDecrypt(revent.Data, key)
		if err != nil {
			return nil, err
		}
		var event VaultEvent
		if err := msgpack.Unmarshal(decrypted, &event); err != nil {
			return nil, err
		}
		event.RemoteTimestamp = tsutil.ParseMillis(revent.Timestamp)
		event.RemoteIndex = revent.Index
		out = append(out, &event)
	}
	return &Vault{Events: out, Index: resp.Index, Truncated: resp.Truncated}, nil
}

func vaultEncrypt(b []byte, key *keys.EdX25519Key) []byte {
	return keys.BoxSeal(b, key.X25519Key().PublicKey(), key.X25519Key())
}

func vaultDecrypt(b []byte, key *keys.EdX25519Key) ([]byte, error) {
	return keys.BoxOpen(b, key.X25519Key().PublicKey(), key.X25519Key())
}

// VaultDelete removes a vault.
func (c *Client) VaultDelete(ctx context.Context, key *keys.EdX25519Key) error {
	path := dstore.Path("vault", key.ID())
	if _, err := c.req(ctx, request{Method: "DELETE", Path: path, Key: key}); err != nil {
		return err
	}
	return nil
}

// VaultExists checks if vault exists.
func (c *Client) VaultExists(ctx context.Context, key *keys.EdX25519Key) (bool, error) {
	path := dstore.Path("vault", key.ID())
	params := url.Values{}
	resp, err := c.req(ctx, request{Method: "HEAD", Path: path, Params: params, Key: key})
	if err != nil {
		return false, err
	}
	if resp == nil {
		return false, nil
	}

	return true, nil
}

// NewVaultEvent creates a new event.
func NewVaultEvent(path string, b []byte) *VaultEvent {
	return &VaultEvent{
		Path: path,
		Data: b,
	}
}
