// Package bridge provides cross-chain asset transfer functionality.
package bridge

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"sync/atomic"
	"time"
)

// Client is a Synor Bridge client for cross-chain transfers.
type Client struct {
	config     Config
	httpClient *http.Client
	closed     atomic.Bool
}

// NewClient creates a new bridge client with the given configuration.
func NewClient(config Config) *Client {
	if config.Endpoint == "" {
		config.Endpoint = DefaultEndpoint
	}
	if config.Timeout == 0 {
		config.Timeout = 60 * time.Second
	}
	if config.Retries == 0 {
		config.Retries = 3
	}

	return &Client{
		config: config,
		httpClient: &http.Client{
			Timeout: config.Timeout,
		},
	}
}

// ==================== Chain Operations ====================

// GetSupportedChains returns all supported chains.
func (c *Client) GetSupportedChains(ctx context.Context) ([]Chain, error) {
	var resp struct {
		Chains []Chain `json:"chains"`
	}
	if err := c.request(ctx, "GET", "/chains", nil, &resp); err != nil {
		return nil, err
	}
	return resp.Chains, nil
}

// GetChain returns chain information by ID.
func (c *Client) GetChain(ctx context.Context, chainID ChainID) (*Chain, error) {
	var chain Chain
	if err := c.request(ctx, "GET", fmt.Sprintf("/chains/%s", chainID), nil, &chain); err != nil {
		return nil, err
	}
	return &chain, nil
}

// IsChainSupported checks if a chain is supported.
func (c *Client) IsChainSupported(ctx context.Context, chainID ChainID) bool {
	chain, err := c.GetChain(ctx, chainID)
	if err != nil {
		return false
	}
	return chain.Supported
}

// ==================== Asset Operations ====================

// GetSupportedAssets returns supported assets for a chain.
func (c *Client) GetSupportedAssets(ctx context.Context, chainID ChainID) ([]Asset, error) {
	var resp struct {
		Assets []Asset `json:"assets"`
	}
	if err := c.request(ctx, "GET", fmt.Sprintf("/chains/%s/assets", chainID), nil, &resp); err != nil {
		return nil, err
	}
	return resp.Assets, nil
}

// GetAsset returns asset information by ID.
func (c *Client) GetAsset(ctx context.Context, assetID string) (*Asset, error) {
	var asset Asset
	if err := c.request(ctx, "GET", fmt.Sprintf("/assets/%s", assetID), nil, &asset); err != nil {
		return nil, err
	}
	return &asset, nil
}

// GetWrappedAsset returns the wrapped asset mapping.
func (c *Client) GetWrappedAsset(ctx context.Context, originalAssetID string, targetChain ChainID) (*WrappedAsset, error) {
	var wrapped WrappedAsset
	path := fmt.Sprintf("/assets/%s/wrapped/%s", originalAssetID, targetChain)
	if err := c.request(ctx, "GET", path, nil, &wrapped); err != nil {
		return nil, err
	}
	return &wrapped, nil
}

// GetWrappedAssets returns all wrapped assets for a chain.
func (c *Client) GetWrappedAssets(ctx context.Context, chainID ChainID) ([]WrappedAsset, error) {
	var resp struct {
		Assets []WrappedAsset `json:"assets"`
	}
	if err := c.request(ctx, "GET", fmt.Sprintf("/chains/%s/wrapped", chainID), nil, &resp); err != nil {
		return nil, err
	}
	return resp.Assets, nil
}

// ==================== Fee & Rate Operations ====================

// EstimateFee estimates the bridge fee for a transfer.
func (c *Client) EstimateFee(ctx context.Context, asset, amount string, sourceChain, targetChain ChainID) (*FeeEstimate, error) {
	body := map[string]interface{}{
		"asset":       asset,
		"amount":      amount,
		"sourceChain": sourceChain,
		"targetChain": targetChain,
	}
	var estimate FeeEstimate
	if err := c.request(ctx, "POST", "/fees/estimate", body, &estimate); err != nil {
		return nil, err
	}
	return &estimate, nil
}

// GetExchangeRate returns the exchange rate between two assets.
func (c *Client) GetExchangeRate(ctx context.Context, fromAsset, toAsset string) (*ExchangeRate, error) {
	var rate ExchangeRate
	path := fmt.Sprintf("/rates/%s/%s", url.PathEscape(fromAsset), url.PathEscape(toAsset))
	if err := c.request(ctx, "GET", path, nil, &rate); err != nil {
		return nil, err
	}
	return &rate, nil
}

// ==================== Lock-Mint Flow ====================

// Lock locks assets on the source chain for cross-chain transfer.
// This is step 1 of the lock-mint flow.
func (c *Client) Lock(ctx context.Context, asset, amount string, targetChain ChainID, opts *LockOptions) (*LockReceipt, error) {
	body := map[string]interface{}{
		"asset":       asset,
		"amount":      amount,
		"targetChain": targetChain,
	}
	if opts != nil {
		if opts.Recipient != "" {
			body["recipient"] = opts.Recipient
		}
		if opts.Deadline != 0 {
			body["deadline"] = opts.Deadline
		}
		if opts.Slippage != 0 {
			body["slippage"] = opts.Slippage
		}
	}

	var receipt LockReceipt
	if err := c.request(ctx, "POST", "/transfers/lock", body, &receipt); err != nil {
		return nil, err
	}
	return &receipt, nil
}

// GetLockProof gets the lock proof for minting.
// This is step 2 of the lock-mint flow.
func (c *Client) GetLockProof(ctx context.Context, lockReceiptID string) (*LockProof, error) {
	var proof LockProof
	path := fmt.Sprintf("/transfers/lock/%s/proof", lockReceiptID)
	if err := c.request(ctx, "GET", path, nil, &proof); err != nil {
		return nil, err
	}
	return &proof, nil
}

// WaitForLockProof waits for the lock proof to be ready.
func (c *Client) WaitForLockProof(ctx context.Context, lockReceiptID string, pollInterval, maxWait time.Duration) (*LockProof, error) {
	if pollInterval == 0 {
		pollInterval = 5 * time.Second
	}
	if maxWait == 0 {
		maxWait = 10 * time.Minute
	}

	deadline := time.Now().Add(maxWait)
	for time.Now().Before(deadline) {
		proof, err := c.GetLockProof(ctx, lockReceiptID)
		if err == nil {
			return proof, nil
		}

		if bridgeErr, ok := err.(*Error); ok && bridgeErr.Code == "CONFIRMATIONS_PENDING" {
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			case <-time.After(pollInterval):
				continue
			}
		}
		return nil, err
	}

	return nil, &Error{Message: "Timeout waiting for lock proof", Code: "CONFIRMATIONS_PENDING"}
}

// Mint mints wrapped tokens on the target chain.
// This is step 3 of the lock-mint flow.
func (c *Client) Mint(ctx context.Context, proof *LockProof, targetAddress string, opts *MintOptions) (*SignedTransaction, error) {
	body := map[string]interface{}{
		"proof":         proof,
		"targetAddress": targetAddress,
	}
	if opts != nil {
		if opts.GasLimit != "" {
			body["gasLimit"] = opts.GasLimit
		}
		if opts.MaxFeePerGas != "" {
			body["maxFeePerGas"] = opts.MaxFeePerGas
		}
		if opts.MaxPriorityFeePerGas != "" {
			body["maxPriorityFeePerGas"] = opts.MaxPriorityFeePerGas
		}
	}

	var tx SignedTransaction
	if err := c.request(ctx, "POST", "/transfers/mint", body, &tx); err != nil {
		return nil, err
	}
	return &tx, nil
}

// ==================== Burn-Unlock Flow ====================

// Burn burns wrapped tokens on the current chain.
// This is step 1 of the burn-unlock flow.
func (c *Client) Burn(ctx context.Context, wrappedAsset, amount string, opts *BurnOptions) (*BurnReceipt, error) {
	body := map[string]interface{}{
		"wrappedAsset": wrappedAsset,
		"amount":       amount,
	}
	if opts != nil {
		if opts.Recipient != "" {
			body["recipient"] = opts.Recipient
		}
		if opts.Deadline != 0 {
			body["deadline"] = opts.Deadline
		}
	}

	var receipt BurnReceipt
	if err := c.request(ctx, "POST", "/transfers/burn", body, &receipt); err != nil {
		return nil, err
	}
	return &receipt, nil
}

// GetBurnProof gets the burn proof for unlocking.
// This is step 2 of the burn-unlock flow.
func (c *Client) GetBurnProof(ctx context.Context, burnReceiptID string) (*BurnProof, error) {
	var proof BurnProof
	path := fmt.Sprintf("/transfers/burn/%s/proof", burnReceiptID)
	if err := c.request(ctx, "GET", path, nil, &proof); err != nil {
		return nil, err
	}
	return &proof, nil
}

// WaitForBurnProof waits for the burn proof to be ready.
func (c *Client) WaitForBurnProof(ctx context.Context, burnReceiptID string, pollInterval, maxWait time.Duration) (*BurnProof, error) {
	if pollInterval == 0 {
		pollInterval = 5 * time.Second
	}
	if maxWait == 0 {
		maxWait = 10 * time.Minute
	}

	deadline := time.Now().Add(maxWait)
	for time.Now().Before(deadline) {
		proof, err := c.GetBurnProof(ctx, burnReceiptID)
		if err == nil {
			return proof, nil
		}

		if bridgeErr, ok := err.(*Error); ok && bridgeErr.Code == "CONFIRMATIONS_PENDING" {
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			case <-time.After(pollInterval):
				continue
			}
		}
		return nil, err
	}

	return nil, &Error{Message: "Timeout waiting for burn proof", Code: "CONFIRMATIONS_PENDING"}
}

// Unlock unlocks original tokens on the source chain.
// This is step 3 of the burn-unlock flow.
func (c *Client) Unlock(ctx context.Context, proof *BurnProof, opts *UnlockOptions) (*SignedTransaction, error) {
	body := map[string]interface{}{
		"proof": proof,
	}
	if opts != nil {
		if opts.GasLimit != "" {
			body["gasLimit"] = opts.GasLimit
		}
		if opts.GasPrice != "" {
			body["gasPrice"] = opts.GasPrice
		}
	}

	var tx SignedTransaction
	if err := c.request(ctx, "POST", "/transfers/unlock", body, &tx); err != nil {
		return nil, err
	}
	return &tx, nil
}

// ==================== Transfer Management ====================

// GetTransfer returns a transfer by ID.
func (c *Client) GetTransfer(ctx context.Context, transferID string) (*Transfer, error) {
	var transfer Transfer
	if err := c.request(ctx, "GET", fmt.Sprintf("/transfers/%s", transferID), nil, &transfer); err != nil {
		return nil, err
	}
	return &transfer, nil
}

// GetTransferStatus returns the status of a transfer.
func (c *Client) GetTransferStatus(ctx context.Context, transferID string) (TransferStatus, error) {
	transfer, err := c.GetTransfer(ctx, transferID)
	if err != nil {
		return "", err
	}
	return transfer.Status, nil
}

// ListTransfers returns transfers matching the filter.
func (c *Client) ListTransfers(ctx context.Context, filter *TransferFilter) ([]Transfer, error) {
	params := url.Values{}
	if filter != nil {
		if filter.Status != "" {
			params.Set("status", string(filter.Status))
		}
		if filter.SourceChain != "" {
			params.Set("sourceChain", string(filter.SourceChain))
		}
		if filter.TargetChain != "" {
			params.Set("targetChain", string(filter.TargetChain))
		}
		if filter.Asset != "" {
			params.Set("asset", filter.Asset)
		}
		if filter.Sender != "" {
			params.Set("sender", filter.Sender)
		}
		if filter.Recipient != "" {
			params.Set("recipient", filter.Recipient)
		}
		if filter.FromDate != nil {
			params.Set("fromDate", strconv.FormatInt(filter.FromDate.Unix(), 10))
		}
		if filter.ToDate != nil {
			params.Set("toDate", strconv.FormatInt(filter.ToDate.Unix(), 10))
		}
		if filter.Limit > 0 {
			params.Set("limit", strconv.Itoa(filter.Limit))
		}
		if filter.Offset > 0 {
			params.Set("offset", strconv.Itoa(filter.Offset))
		}
	}

	path := "/transfers"
	if len(params) > 0 {
		path = "/transfers?" + params.Encode()
	}

	var resp struct {
		Transfers []Transfer `json:"transfers"`
	}
	if err := c.request(ctx, "GET", path, nil, &resp); err != nil {
		return nil, err
	}
	return resp.Transfers, nil
}

// WaitForTransfer waits for a transfer to complete.
func (c *Client) WaitForTransfer(ctx context.Context, transferID string, pollInterval, maxWait time.Duration) (*Transfer, error) {
	if pollInterval == 0 {
		pollInterval = 10 * time.Second
	}
	if maxWait == 0 {
		maxWait = 30 * time.Minute
	}

	finalStatuses := map[TransferStatus]bool{
		TransferStatusCompleted: true,
		TransferStatusFailed:    true,
		TransferStatusRefunded:  true,
	}

	deadline := time.Now().Add(maxWait)
	for time.Now().Before(deadline) {
		transfer, err := c.GetTransfer(ctx, transferID)
		if err != nil {
			return nil, err
		}

		if finalStatuses[transfer.Status] {
			return transfer, nil
		}

		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-time.After(pollInterval):
			continue
		}
	}

	return nil, &Error{Message: "Timeout waiting for transfer completion"}
}

// ==================== Convenience Methods ====================

// BridgeTo executes a complete lock-mint transfer.
func (c *Client) BridgeTo(ctx context.Context, asset, amount string, targetChain ChainID, targetAddress string, lockOpts *LockOptions, mintOpts *MintOptions) (*Transfer, error) {
	// Lock on source chain
	lockReceipt, err := c.Lock(ctx, asset, amount, targetChain, lockOpts)
	if err != nil {
		return nil, err
	}

	if c.config.Debug {
		fmt.Printf("Locked: %s, waiting for confirmations...\n", lockReceipt.ID)
	}

	// Wait for proof
	proof, err := c.WaitForLockProof(ctx, lockReceipt.ID, 0, 0)
	if err != nil {
		return nil, err
	}

	if c.config.Debug {
		fmt.Printf("Proof ready, minting on %s...\n", targetChain)
	}

	// Mint on target chain
	_, err = c.Mint(ctx, proof, targetAddress, mintOpts)
	if err != nil {
		return nil, err
	}

	// Return final transfer status
	return c.WaitForTransfer(ctx, lockReceipt.ID, 0, 0)
}

// BridgeBack executes a complete burn-unlock transfer.
func (c *Client) BridgeBack(ctx context.Context, wrappedAsset, amount string, burnOpts *BurnOptions, unlockOpts *UnlockOptions) (*Transfer, error) {
	// Burn wrapped tokens
	burnReceipt, err := c.Burn(ctx, wrappedAsset, amount, burnOpts)
	if err != nil {
		return nil, err
	}

	if c.config.Debug {
		fmt.Printf("Burned: %s, waiting for confirmations...\n", burnReceipt.ID)
	}

	// Wait for proof
	proof, err := c.WaitForBurnProof(ctx, burnReceipt.ID, 0, 0)
	if err != nil {
		return nil, err
	}

	if c.config.Debug {
		fmt.Printf("Proof ready, unlocking on %s...\n", burnReceipt.TargetChain)
	}

	// Unlock on original chain
	_, err = c.Unlock(ctx, proof, unlockOpts)
	if err != nil {
		return nil, err
	}

	// Return final transfer status
	return c.WaitForTransfer(ctx, burnReceipt.ID, 0, 0)
}

// ==================== Lifecycle ====================

// Close closes the client.
func (c *Client) Close() {
	c.closed.Store(true)
}

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

// HealthCheck performs a health check.
func (c *Client) HealthCheck(ctx context.Context) bool {
	var resp struct {
		Status string `json:"status"`
	}
	if err := c.request(ctx, "GET", "/health", nil, &resp); err != nil {
		return false
	}
	return resp.Status == "healthy"
}

// ==================== Private Methods ====================

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

	var lastErr error
	for attempt := 0; attempt < c.config.Retries; attempt++ {
		err := c.doRequest(ctx, method, path, body, result)
		if err == nil {
			return nil
		}

		if c.config.Debug {
			fmt.Printf("Attempt %d failed: %v\n", attempt+1, err)
		}

		lastErr = err
		if attempt < c.config.Retries-1 {
			time.Sleep(time.Duration(1<<attempt) * time.Second)
		}
	}

	return lastErr
}

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

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

	req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
	if err != nil {
		return err
	}

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

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	if resp.StatusCode >= 400 {
		var errorResp struct {
			Message string `json:"message"`
			Error   string `json:"error"`
			Code    string `json:"code"`
		}
		json.Unmarshal(respBody, &errorResp)

		message := errorResp.Message
		if message == "" {
			message = errorResp.Error
		}
		if message == "" {
			message = fmt.Sprintf("HTTP %d", resp.StatusCode)
		}

		return &Error{
			Message:    message,
			Code:       errorResp.Code,
			StatusCode: resp.StatusCode,
		}
	}

	if result != nil {
		if err := json.Unmarshal(respBody, result); err != nil {
			return err
		}
	}

	return nil
}

// Error represents a bridge error.
type Error struct {
	Message    string
	Code       string
	StatusCode int
}

func (e *Error) Error() string {
	return e.Message
}
