// Package qpq provides the high-level Go API for interacting with a quicproquo server.
//
// It wraps the generated Cap'n Proto types and transport layer into an
// ergonomic client that handles authentication, key management, and messaging.
package qpq

import (
	"context"
	"fmt"

	"quicproquo.dev/sdk/go/proto/node"
	"quicproquo.dev/sdk/go/transport"
)

// Options configures the connection to a quicproquo server.
type Options struct {
	// Addr is the host:port of the server (e.g. "127.0.0.1:5001").
	Addr string

	// InsecureSkipVerify disables TLS certificate verification (dev mode only).
	InsecureSkipVerify bool

	// CACertPath is the path to a PEM-encoded CA certificate for production use.
	CACertPath string
}

// Message represents a received message envelope.
type Message struct {
	Seq  uint64
	Data []byte
}

// Client is the high-level quicproquo client.
type Client struct {
	conn     *transport.Connection
	token    []byte // session token from OPAQUE login
	deviceID []byte // optional device ID
}

// Connect establishes a connection to a qpq server.
func Connect(ctx context.Context, opts Options) (*Client, error) {
	conn, err := transport.Connect(ctx, transport.ConnectOptions{
		Addr:               opts.Addr,
		InsecureSkipVerify: opts.InsecureSkipVerify,
		CACertPath:         opts.CACertPath,
	})
	if err != nil {
		return nil, fmt.Errorf("qpq: connect: %w", err)
	}
	return &Client{conn: conn}, nil
}

// Close disconnects from the server.
func (c *Client) Close() error {
	return c.conn.Close()
}

// SetSessionToken sets a pre-existing session token for authentication.
// Use this when you have already performed OPAQUE login externally.
func (c *Client) SetSessionToken(token []byte) {
	c.token = token
}

// SetDeviceID sets the device ID sent with each authenticated RPC call.
func (c *Client) SetDeviceID(id []byte) {
	c.deviceID = id
}

// setAuth populates an Auth struct on an RPC params message.
func (c *Client) setAuth(auth node.Auth) error {
	auth.SetVersion(1)
	if err := auth.SetAccessToken(c.token); err != nil {
		return fmt.Errorf("set access token: %w", err)
	}
	if len(c.deviceID) > 0 {
		if err := auth.SetDeviceId(c.deviceID); err != nil {
			return fmt.Errorf("set device id: %w", err)
		}
	}
	return nil
}

// Health checks server health and returns the status string.
func (c *Client) Health(ctx context.Context) (string, error) {
	future, release := c.conn.Client().Health(ctx, nil)
	defer release()

	res, err := future.Struct()
	if err != nil {
		return "", fmt.Errorf("qpq: health: %w", err)
	}
	status, err := res.Status()
	if err != nil {
		return "", fmt.Errorf("qpq: health: read status: %w", err)
	}
	return status, nil
}

// ResolveUser looks up a username and returns their identity key.
func (c *Client) ResolveUser(ctx context.Context, username string) (identityKey []byte, err error) {
	future, release := c.conn.Client().ResolveUser(ctx, func(p node.NodeService_resolveUser_Params) error {
		if err := p.SetUsername(username); err != nil {
			return err
		}
		auth, err := p.NewAuth()
		if err != nil {
			return err
		}
		return c.setAuth(auth)
	})
	defer release()

	res, err := future.Struct()
	if err != nil {
		return nil, fmt.Errorf("qpq: resolve user %q: %w", username, err)
	}
	key, err := res.IdentityKey()
	if err != nil {
		return nil, fmt.Errorf("qpq: resolve user %q: read identity key: %w", username, err)
	}
	// Copy the key out of the capnp message buffer.
	out := make([]byte, len(key))
	copy(out, key)
	return out, nil
}

// CreateChannel creates a 1:1 DM channel with a peer identified by their identity key.
func (c *Client) CreateChannel(ctx context.Context, peerKey []byte) (channelID []byte, wasNew bool, err error) {
	future, release := c.conn.Client().CreateChannel(ctx, func(p node.NodeService_createChannel_Params) error {
		if err := p.SetPeerKey(peerKey); err != nil {
			return err
		}
		auth, err := p.NewAuth()
		if err != nil {
			return err
		}
		return c.setAuth(auth)
	})
	defer release()

	res, err := future.Struct()
	if err != nil {
		return nil, false, fmt.Errorf("qpq: create channel: %w", err)
	}
	chID, err := res.ChannelId()
	if err != nil {
		return nil, false, fmt.Errorf("qpq: create channel: read channel id: %w", err)
	}
	out := make([]byte, len(chID))
	copy(out, chID)
	return out, res.WasNew(), nil
}

// Send enqueues a message payload to a recipient identified by their identity key.
func (c *Client) Send(ctx context.Context, recipientKey, payload []byte) (seq uint64, err error) {
	return c.sendInternal(ctx, recipientKey, payload, 0)
}

// SendWithTTL enqueues a disappearing message with a time-to-live in seconds.
func (c *Client) SendWithTTL(ctx context.Context, recipientKey, payload []byte, ttlSecs uint32) (seq uint64, err error) {
	return c.sendInternal(ctx, recipientKey, payload, ttlSecs)
}

func (c *Client) sendInternal(ctx context.Context, recipientKey, payload []byte, ttlSecs uint32) (uint64, error) {
	future, release := c.conn.Client().Enqueue(ctx, func(p node.NodeService_enqueue_Params) error {
		if err := p.SetRecipientKey(recipientKey); err != nil {
			return err
		}
		if err := p.SetPayload(payload); err != nil {
			return err
		}
		p.SetVersion(1)
		if ttlSecs > 0 {
			p.SetTtlSecs(ttlSecs)
		}
		auth, err := p.NewAuth()
		if err != nil {
			return err
		}
		return c.setAuth(auth)
	})
	defer release()

	res, err := future.Struct()
	if err != nil {
		return 0, fmt.Errorf("qpq: send: %w", err)
	}
	return res.Seq(), nil
}

// Receive fetches queued messages for the given recipient key.
func (c *Client) Receive(ctx context.Context, recipientKey []byte) ([]Message, error) {
	future, release := c.conn.Client().Fetch(ctx, func(p node.NodeService_fetch_Params) error {
		if err := p.SetRecipientKey(recipientKey); err != nil {
			return err
		}
		p.SetVersion(1)
		auth, err := p.NewAuth()
		if err != nil {
			return err
		}
		return c.setAuth(auth)
	})
	defer release()

	res, err := future.Struct()
	if err != nil {
		return nil, fmt.Errorf("qpq: receive: %w", err)
	}
	return extractMessages(res.Payloads())
}

// ReceiveWait long-polls for messages with a timeout in milliseconds.
func (c *Client) ReceiveWait(ctx context.Context, recipientKey []byte, timeoutMs uint64) ([]Message, error) {
	future, release := c.conn.Client().FetchWait(ctx, func(p node.NodeService_fetchWait_Params) error {
		if err := p.SetRecipientKey(recipientKey); err != nil {
			return err
		}
		p.SetVersion(1)
		p.SetTimeoutMs(timeoutMs)
		auth, err := p.NewAuth()
		if err != nil {
			return err
		}
		return c.setAuth(auth)
	})
	defer release()

	res, err := future.Struct()
	if err != nil {
		return nil, fmt.Errorf("qpq: receive wait: %w", err)
	}
	return extractMessages(res.Payloads())
}

// DeleteAccount permanently deletes the authenticated user's account.
func (c *Client) DeleteAccount(ctx context.Context) error {
	future, release := c.conn.Client().DeleteAccount(ctx, func(p node.NodeService_deleteAccount_Params) error {
		auth, err := p.NewAuth()
		if err != nil {
			return err
		}
		return c.setAuth(auth)
	})
	defer release()

	res, err := future.Struct()
	if err != nil {
		return fmt.Errorf("qpq: delete account: %w", err)
	}
	if !res.Success() {
		return fmt.Errorf("qpq: delete account: server returned success=false")
	}
	return nil
}

// RegisterStart initiates OPAQUE registration and returns the server's response bytes.
//
// The OPAQUE protocol requires client-side cryptographic operations.
// The request parameter must be the serialized OPAQUE RegistrationRequest
// generated by an OPAQUE client library (e.g. github.com/cloudflare/circl/opaque).
// Process the returned server response with your OPAQUE library to produce
// the upload bytes for RegisterFinish.
func (c *Client) RegisterStart(ctx context.Context, username string, request []byte) (serverResponse []byte, err error) {
	future, release := c.conn.Client().OpaqueRegisterStart(ctx, func(p node.NodeService_opaqueRegisterStart_Params) error {
		if err := p.SetUsername(username); err != nil {
			return err
		}
		return p.SetRequest(request)
	})
	defer release()

	res, err := future.Struct()
	if err != nil {
		return nil, fmt.Errorf("qpq: register start: %w", err)
	}
	resp, err := res.Response()
	if err != nil {
		return nil, fmt.Errorf("qpq: register start: read response: %w", err)
	}
	out := make([]byte, len(resp))
	copy(out, resp)
	return out, nil
}

// RegisterFinish completes OPAQUE registration with the upload and identity key.
func (c *Client) RegisterFinish(ctx context.Context, username string, upload, identityKey []byte) error {
	future, release := c.conn.Client().OpaqueRegisterFinish(ctx, func(p node.NodeService_opaqueRegisterFinish_Params) error {
		if err := p.SetUsername(username); err != nil {
			return err
		}
		if err := p.SetUpload(upload); err != nil {
			return err
		}
		return p.SetIdentityKey(identityKey)
	})
	defer release()

	_, err := future.Struct()
	if err != nil {
		return fmt.Errorf("qpq: register finish: %w", err)
	}
	return nil
}

// LoginStart initiates OPAQUE login and returns the server's response bytes.
func (c *Client) LoginStart(ctx context.Context, username string, request []byte) (serverResponse []byte, err error) {
	future, release := c.conn.Client().OpaqueLoginStart(ctx, func(p node.NodeService_opaqueLoginStart_Params) error {
		if err := p.SetUsername(username); err != nil {
			return err
		}
		return p.SetRequest(request)
	})
	defer release()

	res, err := future.Struct()
	if err != nil {
		return nil, fmt.Errorf("qpq: login start: %w", err)
	}
	resp, err := res.Response()
	if err != nil {
		return nil, fmt.Errorf("qpq: login start: read response: %w", err)
	}
	out := make([]byte, len(resp))
	copy(out, resp)
	return out, nil
}

// LoginFinish completes OPAQUE login and stores the session token.
// The finalization parameter is the OPAQUE finalization message from
// your OPAQUE client library. The identityKey is your public identity key.
func (c *Client) LoginFinish(ctx context.Context, username string, finalization, identityKey []byte) error {
	future, release := c.conn.Client().OpaqueLoginFinish(ctx, func(p node.NodeService_opaqueLoginFinish_Params) error {
		if err := p.SetUsername(username); err != nil {
			return err
		}
		if err := p.SetFinalization(finalization); err != nil {
			return err
		}
		return p.SetIdentityKey(identityKey)
	})
	defer release()

	res, err := future.Struct()
	if err != nil {
		return fmt.Errorf("qpq: login finish: %w", err)
	}
	token, err := res.SessionToken()
	if err != nil {
		return fmt.Errorf("qpq: login finish: read session token: %w", err)
	}
	c.token = make([]byte, len(token))
	copy(c.token, token)
	return nil
}

// extractMessages converts a Cap'n Proto Envelope_List into a slice of Message.
func extractMessages(envList node.Envelope_List, listErr error) ([]Message, error) {
	if listErr != nil {
		return nil, listErr
	}
	msgs := make([]Message, envList.Len())
	for i := range msgs {
		env := envList.At(i)
		data, err := env.Data()
		if err != nil {
			return nil, fmt.Errorf("read message %d data: %w", i, err)
		}
		dataCopy := make([]byte, len(data))
		copy(dataCopy, data)
		msgs[i] = Message{
			Seq:  env.Seq(),
			Data: dataCopy,
		}
	}
	return msgs, nil
}
