// Package rpc provides a Go SDK for Synor blockchain RPC operations.
//
// Query blocks, transactions, and chain state with WebSocket subscription support.
//
// Example:
//
//	client := rpc.NewClient("your-api-key")
//	block, err := client.GetLatestBlock(ctx)
//	fmt.Println("Height:", block.Height)
package rpc

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"time"

	"github.com/gorilla/websocket"
)

// Version of the SDK.
const Version = "0.1.0"

// Default endpoints.
const (
	DefaultEndpoint   = "https://rpc.synor.cc/api/v1"
	DefaultWSEndpoint = "wss://rpc.synor.cc/ws"
)

// Network type.
type Network string

const (
	Mainnet Network = "mainnet"
	Testnet Network = "testnet"
)

// Priority levels.
type Priority string

const (
	Low    Priority = "low"
	Medium Priority = "medium"
	High   Priority = "high"
	Urgent Priority = "urgent"
)

// TransactionStatus represents transaction states.
type TransactionStatus string

const (
	Pending   TransactionStatus = "pending"
	Confirmed TransactionStatus = "confirmed"
	Failed    TransactionStatus = "failed"
	Replaced  TransactionStatus = "replaced"
)

// Config holds client configuration.
type Config struct {
	APIKey     string
	Endpoint   string
	WSEndpoint string
	Network    Network
	Timeout    time.Duration
	Debug      bool
}

// DefaultConfig returns a default configuration.
func DefaultConfig(apiKey string) Config {
	return Config{
		APIKey:     apiKey,
		Endpoint:   DefaultEndpoint,
		WSEndpoint: DefaultWSEndpoint,
		Network:    Mainnet,
		Timeout:    30 * time.Second,
		Debug:      false,
	}
}

// Client is the Synor RPC client.
type Client struct {
	config     Config
	httpClient *http.Client
	wsConn     *websocket.Conn
	subs       map[string]func(json.RawMessage)
}

// NewClient creates a new client with the given API key.
func NewClient(apiKey string) *Client {
	return NewClientWithConfig(DefaultConfig(apiKey))
}

// NewClientWithConfig creates a new client with custom configuration.
func NewClientWithConfig(config Config) *Client {
	return &Client{
		config: config,
		httpClient: &http.Client{
			Timeout: config.Timeout,
		},
		subs: make(map[string]func(json.RawMessage)),
	}
}

// BlockHeader represents a block header.
type BlockHeader struct {
	Hash         string `json:"hash"`
	Height       int64  `json:"height"`
	Version      int    `json:"version"`
	PreviousHash string `json:"previousHash"`
	MerkleRoot   string `json:"merkleRoot"`
	Timestamp    int64  `json:"timestamp"`
	Difficulty   string `json:"difficulty"`
	Nonce        uint64 `json:"nonce"`
}

// Block represents a full block.
type Block struct {
	BlockHeader
	Transactions []string `json:"transactions"`
	Size         int      `json:"size"`
	Weight       int      `json:"weight"`
	TxCount      int      `json:"txCount"`
}

// Transaction represents a transaction.
type Transaction struct {
	TxID          string            `json:"txid"`
	BlockHash     string            `json:"blockHash,omitempty"`
	BlockHeight   int64             `json:"blockHeight,omitempty"`
	Confirmations int               `json:"confirmations"`
	Timestamp     int64             `json:"timestamp,omitempty"`
	Status        TransactionStatus `json:"status"`
	Raw           string            `json:"raw,omitempty"`
	Size          int               `json:"size"`
	Fee           string            `json:"fee"`
}

// FeeEstimate represents a fee estimation.
type FeeEstimate struct {
	Priority        Priority `json:"priority"`
	FeeRate         string   `json:"feeRate"`
	EstimatedBlocks int      `json:"estimatedBlocks"`
}

// ChainInfo represents chain information.
type ChainInfo struct {
	Chain         string  `json:"chain"`
	Network       string  `json:"network"`
	Height        int64   `json:"height"`
	BestBlockHash string  `json:"bestBlockHash"`
	Difficulty    string  `json:"difficulty"`
	MedianTime    int64   `json:"medianTime"`
	ChainWork     string  `json:"chainWork"`
	Syncing       bool    `json:"syncing"`
	SyncProgress  float64 `json:"syncProgress"`
}

// MempoolInfo represents mempool information.
type MempoolInfo struct {
	Size       int    `json:"size"`
	Bytes      int    `json:"bytes"`
	Usage      int    `json:"usage"`
	MaxMempool int    `json:"maxMempool"`
	MinFee     string `json:"minFee"`
}

// UTXO represents an unspent transaction output.
type UTXO struct {
	TxID          string `json:"txid"`
	Vout          int    `json:"vout"`
	Amount        string `json:"amount"`
	Address       string `json:"address"`
	Confirmations int    `json:"confirmations"`
	ScriptPubKey  string `json:"scriptPubKey,omitempty"`
}

// Balance represents balance information.
type Balance struct {
	Confirmed   string `json:"confirmed"`
	Unconfirmed string `json:"unconfirmed"`
	Total       string `json:"total"`
}

// GetBlock gets a block by hash or height.
func (c *Client) GetBlock(ctx context.Context, hashOrHeight interface{}) (*Block, error) {
	var path string
	switch v := hashOrHeight.(type) {
	case int, int64:
		path = fmt.Sprintf("/blocks/height/%d", v)
	case string:
		path = fmt.Sprintf("/blocks/%s", v)
	default:
		return nil, fmt.Errorf("invalid hashOrHeight type")
	}

	var block Block
	if err := c.request(ctx, "GET", path, nil, &block); err != nil {
		return nil, err
	}
	return &block, nil
}

// GetLatestBlock gets the latest block.
func (c *Client) GetLatestBlock(ctx context.Context) (*Block, error) {
	var block Block
	if err := c.request(ctx, "GET", "/blocks/latest", nil, &block); err != nil {
		return nil, err
	}
	return &block, nil
}

// GetBlockHeader gets a block header.
func (c *Client) GetBlockHeader(ctx context.Context, hashOrHeight interface{}) (*BlockHeader, error) {
	var path string
	switch v := hashOrHeight.(type) {
	case int, int64:
		path = fmt.Sprintf("/blocks/height/%d/header", v)
	case string:
		path = fmt.Sprintf("/blocks/%s/header", v)
	default:
		return nil, fmt.Errorf("invalid hashOrHeight type")
	}

	var header BlockHeader
	if err := c.request(ctx, "GET", path, nil, &header); err != nil {
		return nil, err
	}
	return &header, nil
}

// GetTransaction gets a transaction by hash.
func (c *Client) GetTransaction(ctx context.Context, txid string) (*Transaction, error) {
	var tx Transaction
	if err := c.request(ctx, "GET", "/transactions/"+txid, nil, &tx); err != nil {
		return nil, err
	}
	return &tx, nil
}

// SendRawTransaction sends a raw transaction.
func (c *Client) SendRawTransaction(ctx context.Context, rawTx string) (string, error) {
	var resp struct {
		TxID string `json:"txid"`
	}
	if err := c.request(ctx, "POST", "/transactions", map[string]string{"raw": rawTx}, &resp); err != nil {
		return "", err
	}
	return resp.TxID, nil
}

// EstimateFee estimates the transaction fee.
func (c *Client) EstimateFee(ctx context.Context, priority Priority) (*FeeEstimate, error) {
	path := fmt.Sprintf("/fees/estimate?priority=%s", priority)
	var estimate FeeEstimate
	if err := c.request(ctx, "GET", path, nil, &estimate); err != nil {
		return nil, err
	}
	return &estimate, nil
}

// GetChainInfo gets chain information.
func (c *Client) GetChainInfo(ctx context.Context) (*ChainInfo, error) {
	var info ChainInfo
	if err := c.request(ctx, "GET", "/chain", nil, &info); err != nil {
		return nil, err
	}
	return &info, nil
}

// GetMempoolInfo gets mempool information.
func (c *Client) GetMempoolInfo(ctx context.Context) (*MempoolInfo, error) {
	var info MempoolInfo
	if err := c.request(ctx, "GET", "/mempool", nil, &info); err != nil {
		return nil, err
	}
	return &info, nil
}

// GetUTXOs gets UTXOs for an address.
func (c *Client) GetUTXOs(ctx context.Context, address string) ([]UTXO, error) {
	var resp struct {
		UTXOs []UTXO `json:"utxos"`
	}
	if err := c.request(ctx, "GET", "/addresses/"+address+"/utxos", nil, &resp); err != nil {
		return nil, err
	}
	return resp.UTXOs, nil
}

// GetBalance gets the balance for an address.
func (c *Client) GetBalance(ctx context.Context, address string) (*Balance, error) {
	var balance Balance
	if err := c.request(ctx, "GET", "/addresses/"+address+"/balance", nil, &balance); err != nil {
		return nil, err
	}
	return &balance, nil
}

// Subscription represents a WebSocket subscription.
type Subscription struct {
	ID          string
	Type        string
	CreatedAt   int64
	Unsubscribe func()
}

// SubscribeBlocks subscribes to new blocks.
func (c *Client) SubscribeBlocks(ctx context.Context, callback func(*Block)) (*Subscription, error) {
	return c.subscribe(ctx, "blocks", nil, func(data json.RawMessage) {
		var resp struct {
			Block Block `json:"block"`
		}
		if err := json.Unmarshal(data, &resp); err == nil {
			callback(&resp.Block)
		}
	})
}

// SubscribeAddress subscribes to address transactions.
func (c *Client) SubscribeAddress(ctx context.Context, address string, callback func(*Transaction)) (*Subscription, error) {
	return c.subscribe(ctx, "address", map[string]string{"address": address}, func(data json.RawMessage) {
		var resp struct {
			Transaction Transaction `json:"transaction"`
		}
		if err := json.Unmarshal(data, &resp); err == nil {
			callback(&resp.Transaction)
		}
	})
}

func (c *Client) subscribe(ctx context.Context, subType string, params map[string]string, callback func(json.RawMessage)) (*Subscription, error) {
	if err := c.ensureWebSocket(ctx); err != nil {
		return nil, err
	}

	subID := fmt.Sprintf("%s_%d", subType, time.Now().UnixMilli())
	c.subs[subID] = callback

	msg := map[string]interface{}{
		"type":         "subscribe",
		"id":           subID,
		"subscription": subType,
	}
	for k, v := range params {
		msg[k] = v
	}

	if err := c.wsConn.WriteJSON(msg); err != nil {
		return nil, err
	}

	return &Subscription{
		ID:        subID,
		Type:      subType,
		CreatedAt: time.Now().UnixMilli(),
		Unsubscribe: func() {
			delete(c.subs, subID)
			c.wsConn.WriteJSON(map[string]string{"type": "unsubscribe", "id": subID})
		},
	}, nil
}

func (c *Client) ensureWebSocket(ctx context.Context) error {
	if c.wsConn != nil {
		return nil
	}

	u, _ := url.Parse(c.config.WSEndpoint)
	q := u.Query()
	q.Set("apiKey", c.config.APIKey)
	q.Set("network", string(c.config.Network))
	u.RawQuery = q.Encode()

	conn, _, err := websocket.DefaultDialer.DialContext(ctx, u.String(), nil)
	if err != nil {
		return fmt.Errorf("websocket connection failed: %w", err)
	}

	c.wsConn = conn

	go c.wsListener()

	return nil
}

func (c *Client) wsListener() {
	for {
		_, message, err := c.wsConn.ReadMessage()
		if err != nil {
			return
		}

		var msg struct {
			SubscriptionID string          `json:"subscriptionId"`
			Data           json.RawMessage `json:"data"`
		}
		if err := json.Unmarshal(message, &msg); err != nil {
			continue
		}

		if callback, ok := c.subs[msg.SubscriptionID]; ok {
			callback(msg.Data)
		}
	}
}

// Close closes the client.
func (c *Client) Close() {
	if c.wsConn != nil {
		c.wsConn.Close()
	}
}

func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
	reqURL := c.config.Endpoint + path

	var bodyReader io.Reader
	if body != nil {
		bodyBytes, err := json.Marshal(body)
		if err != nil {
			return fmt.Errorf("failed to marshal request: %w", err)
		}
		bodyReader = bytes.NewReader(bodyBytes)
	}

	req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Network", string(c.config.Network))

	if c.config.Debug {
		fmt.Printf("[SynorRpc] %s %s\n", method, path)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		var errResp struct {
			Message string `json:"message"`
			Code    string `json:"code"`
		}
		json.NewDecoder(resp.Body).Decode(&errResp)
		return &RpcError{
			Message:    errResp.Message,
			StatusCode: resp.StatusCode,
			Code:       errResp.Code,
		}
	}

	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("failed to decode response: %w", err)
		}
	}

	return nil
}

// RpcError represents an API error.
type RpcError struct {
	Message    string
	StatusCode int
	Code       string
}

func (e *RpcError) Error() string {
	return fmt.Sprintf("rpc: %s (status %d)", e.Message, e.StatusCode)
}
