// Package ibc provides the Synor IBC SDK for Go.
package ibc

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

// SynorIbc is the main IBC client
type SynorIbc struct {
	config Config
	client *http.Client
	closed bool

	// Sub-clients
	Clients     *LightClientClient
	Connections *ConnectionsClient
	Channels    *ChannelsClient
	Packets     *PacketsClient
	Transfer    *TransferClient
	Swaps       *SwapsClient
}

// NewSynorIbc creates a new IBC client
func NewSynorIbc(config Config) *SynorIbc {
	ibc := &SynorIbc{
		config: config,
		client: &http.Client{Timeout: config.Timeout},
	}
	ibc.Clients = &LightClientClient{ibc: ibc}
	ibc.Connections = &ConnectionsClient{ibc: ibc}
	ibc.Channels = &ChannelsClient{ibc: ibc}
	ibc.Packets = &PacketsClient{ibc: ibc}
	ibc.Transfer = &TransferClient{ibc: ibc}
	ibc.Swaps = &SwapsClient{ibc: ibc}
	return ibc
}

// ChainID returns the chain ID
func (c *SynorIbc) ChainID() string {
	return c.config.ChainID
}

// GetChainInfo returns chain information
func (c *SynorIbc) GetChainInfo(ctx context.Context) (map[string]interface{}, error) {
	var result map[string]interface{}
	err := c.get(ctx, "/chain", &result)
	return result, err
}

// GetHeight returns current height
func (c *SynorIbc) GetHeight(ctx context.Context) (Height, error) {
	var result Height
	err := c.get(ctx, "/chain/height", &result)
	return result, err
}

// HealthCheck performs a health check
func (c *SynorIbc) HealthCheck(ctx context.Context) bool {
	var result map[string]string
	if err := c.get(ctx, "/health", &result); err != nil {
		return false
	}
	return result["status"] == "healthy"
}

// Close closes the client
func (c *SynorIbc) Close() {
	c.closed = true
}

// IsClosed returns whether client is closed
func (c *SynorIbc) IsClosed() bool {
	return c.closed
}

func (c *SynorIbc) get(ctx context.Context, path string, result interface{}) error {
	return c.request(ctx, "GET", path, nil, result)
}

func (c *SynorIbc) post(ctx context.Context, path string, body, result interface{}) error {
	return c.request(ctx, "POST", path, body, result)
}

func (c *SynorIbc) delete(ctx context.Context, path string, result interface{}) error {
	return c.request(ctx, "DELETE", path, nil, result)
}

func (c *SynorIbc) request(ctx context.Context, method, path string, body, result interface{}) error {
	if c.closed {
		return &IbcError{Message: "Client has been closed", Code: "CLIENT_CLOSED"}
	}

	var bodyReader io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return err
		}
		bodyReader = bytes.NewReader(data)
	}

	url := c.config.Endpoint + path
	var lastErr error

	for attempt := 0; attempt <= c.config.Retries; attempt++ {
		req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
		if err != nil {
			return err
		}

		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
		req.Header.Set("X-SDK-Version", "go/0.1.0")
		req.Header.Set("X-Chain-Id", c.config.ChainID)

		resp, err := c.client.Do(req)
		if err != nil {
			lastErr = err
			time.Sleep(time.Duration(1<<attempt) * 100 * time.Millisecond)
			continue
		}
		defer resp.Body.Close()

		if resp.StatusCode >= 400 {
			var errResp map[string]interface{}
			json.NewDecoder(resp.Body).Decode(&errResp)
			msg := fmt.Sprintf("HTTP %d", resp.StatusCode)
			if m, ok := errResp["message"].(string); ok {
				msg = m
			}
			code := ""
			if c, ok := errResp["code"].(string); ok {
				code = c
			}
			return &IbcError{Message: msg, Code: code, Status: resp.StatusCode}
		}

		if result != nil {
			return json.NewDecoder(resp.Body).Decode(result)
		}
		return nil
	}

	return lastErr
}

// LightClientClient handles light client operations
type LightClientClient struct {
	ibc *SynorIbc
}

// Create creates a new light client
func (c *LightClientClient) Create(ctx context.Context, clientType ClientType, clientState ClientState, consensusState ConsensusState) (ClientId, error) {
	var result struct {
		ClientID string `json:"client_id"`
	}
	err := c.ibc.post(ctx, "/clients", map[string]interface{}{
		"client_type":     clientType,
		"client_state":    clientState,
		"consensus_state": consensusState,
	}, &result)
	return ClientId{ID: result.ClientID}, err
}

// Update updates a light client
func (c *LightClientClient) Update(ctx context.Context, clientId ClientId, header Header) (Height, error) {
	var result Height
	err := c.ibc.post(ctx, "/clients/"+clientId.ID+"/update", map[string]interface{}{
		"header": header,
	}, &result)
	return result, err
}

// GetState returns client state
func (c *LightClientClient) GetState(ctx context.Context, clientId ClientId) (ClientState, error) {
	var result ClientState
	err := c.ibc.get(ctx, "/clients/"+clientId.ID+"/state", &result)
	return result, err
}

// List returns all clients
func (c *LightClientClient) List(ctx context.Context) ([]map[string]interface{}, error) {
	var result []map[string]interface{}
	err := c.ibc.get(ctx, "/clients", &result)
	return result, err
}

// ConnectionsClient handles connection operations
type ConnectionsClient struct {
	ibc *SynorIbc
}

// OpenInit initializes a connection
func (c *ConnectionsClient) OpenInit(ctx context.Context, clientId, counterpartyClientId ClientId) (ConnectionId, error) {
	var result struct {
		ConnectionID string `json:"connection_id"`
	}
	err := c.ibc.post(ctx, "/connections/init", map[string]interface{}{
		"client_id":              clientId.ID,
		"counterparty_client_id": counterpartyClientId.ID,
	}, &result)
	return ConnectionId{ID: result.ConnectionID}, err
}

// Get returns a connection
func (c *ConnectionsClient) Get(ctx context.Context, connectionId ConnectionId) (ConnectionEnd, error) {
	var result ConnectionEnd
	err := c.ibc.get(ctx, "/connections/"+connectionId.ID, &result)
	return result, err
}

// List returns all connections
func (c *ConnectionsClient) List(ctx context.Context) ([]map[string]interface{}, error) {
	var result []map[string]interface{}
	err := c.ibc.get(ctx, "/connections", &result)
	return result, err
}

// ChannelsClient handles channel operations
type ChannelsClient struct {
	ibc *SynorIbc
}

// BindPort binds a port
func (c *ChannelsClient) BindPort(ctx context.Context, portId PortId, module string) error {
	return c.ibc.post(ctx, "/ports/bind", map[string]interface{}{
		"port_id": portId.ID,
		"module":  module,
	}, nil)
}

// OpenInit initializes a channel
func (c *ChannelsClient) OpenInit(ctx context.Context, portId PortId, ordering ChannelOrder, connectionId ConnectionId, counterpartyPort PortId, version string) (ChannelId, error) {
	var result struct {
		ChannelID string `json:"channel_id"`
	}
	err := c.ibc.post(ctx, "/channels/init", map[string]interface{}{
		"port_id":           portId.ID,
		"ordering":          ordering,
		"connection_id":     connectionId.ID,
		"counterparty_port": counterpartyPort.ID,
		"version":           version,
	}, &result)
	return ChannelId{ID: result.ChannelID}, err
}

// Get returns a channel
func (c *ChannelsClient) Get(ctx context.Context, portId PortId, channelId ChannelId) (Channel, error) {
	var result Channel
	err := c.ibc.get(ctx, "/channels/"+portId.ID+"/"+channelId.ID, &result)
	return result, err
}

// List returns all channels
func (c *ChannelsClient) List(ctx context.Context) ([]map[string]interface{}, error) {
	var result []map[string]interface{}
	err := c.ibc.get(ctx, "/channels", &result)
	return result, err
}

// PacketsClient handles packet operations
type PacketsClient struct {
	ibc *SynorIbc
}

// Send sends a packet
func (c *PacketsClient) Send(ctx context.Context, sourcePort PortId, sourceChannel ChannelId, data []byte, timeout Timeout) (map[string]interface{}, error) {
	var result map[string]interface{}
	err := c.ibc.post(ctx, "/packets/send", map[string]interface{}{
		"source_port":       sourcePort.ID,
		"source_channel":    sourceChannel.ID,
		"data":              base64.StdEncoding.EncodeToString(data),
		"timeout_height":    timeout.Height,
		"timeout_timestamp": timeout.Timestamp,
	}, &result)
	return result, err
}

// TransferClient handles ICS-20 transfers
type TransferClient struct {
	ibc *SynorIbc
}

// Transfer sends tokens to another chain
func (c *TransferClient) Transfer(ctx context.Context, sourcePort, sourceChannel, denom, amount, sender, receiver string, timeout *Timeout, memo string) (map[string]interface{}, error) {
	body := map[string]interface{}{
		"source_port":    sourcePort,
		"source_channel": sourceChannel,
		"token": map[string]string{
			"denom":  denom,
			"amount": amount,
		},
		"sender":   sender,
		"receiver": receiver,
		"memo":     memo,
	}
	if timeout != nil {
		body["timeout_height"] = timeout.Height
		body["timeout_timestamp"] = timeout.Timestamp
	}
	var result map[string]interface{}
	err := c.ibc.post(ctx, "/transfer", body, &result)
	return result, err
}

// GetDenomTrace returns denom trace
func (c *TransferClient) GetDenomTrace(ctx context.Context, ibcDenom string) (map[string]string, error) {
	var result map[string]string
	err := c.ibc.get(ctx, "/transfer/denom_trace/"+ibcDenom, &result)
	return result, err
}

// SwapsClient handles atomic swap operations
type SwapsClient struct {
	ibc *SynorIbc
}

// Initiate starts an atomic swap
func (c *SwapsClient) Initiate(ctx context.Context, responder string, initiatorAsset, responderAsset SwapAsset) (map[string]interface{}, error) {
	var result map[string]interface{}
	err := c.ibc.post(ctx, "/swaps/initiate", map[string]interface{}{
		"responder":        responder,
		"initiator_asset":  initiatorAsset,
		"responder_asset":  responderAsset,
	}, &result)
	return result, err
}

// Lock locks the initiator's tokens
func (c *SwapsClient) Lock(ctx context.Context, swapId SwapId) error {
	return c.ibc.post(ctx, "/swaps/"+swapId.ID+"/lock", nil, nil)
}

// Respond responds to a swap
func (c *SwapsClient) Respond(ctx context.Context, swapId SwapId, asset SwapAsset) (Htlc, error) {
	var result Htlc
	err := c.ibc.post(ctx, "/swaps/"+swapId.ID+"/respond", map[string]interface{}{
		"asset": asset,
	}, &result)
	return result, err
}

// Claim claims tokens with secret
func (c *SwapsClient) Claim(ctx context.Context, swapId SwapId, secret []byte) (map[string]interface{}, error) {
	var result map[string]interface{}
	err := c.ibc.post(ctx, "/swaps/"+swapId.ID+"/claim", map[string]interface{}{
		"secret": base64.StdEncoding.EncodeToString(secret),
	}, &result)
	return result, err
}

// Refund refunds an expired swap
func (c *SwapsClient) Refund(ctx context.Context, swapId SwapId) (map[string]interface{}, error) {
	var result map[string]interface{}
	err := c.ibc.post(ctx, "/swaps/"+swapId.ID+"/refund", nil, &result)
	return result, err
}

// Get returns a swap
func (c *SwapsClient) Get(ctx context.Context, swapId SwapId) (AtomicSwap, error) {
	var result AtomicSwap
	err := c.ibc.get(ctx, "/swaps/"+swapId.ID, &result)
	return result, err
}

// ListActive returns active swaps
func (c *SwapsClient) ListActive(ctx context.Context) ([]AtomicSwap, error) {
	var result []AtomicSwap
	err := c.ibc.get(ctx, "/swaps/active", &result)
	return result, err
}

// VerifySecret verifies a hashlock with secret
func (c *SwapsClient) VerifySecret(hashlock Hashlock, secret []byte) bool {
	hash := sha256.Sum256(secret)
	return bytes.Equal(hashlock.Hash, hash[:])
}
