// Package storage provides a Go SDK for Synor Storage operations.
//
// Decentralized storage, pinning, and content retrieval on the Synor network.
//
// Example:
//
//	client := storage.NewClient("your-api-key")
//	result, err := client.Upload(ctx, []byte("Hello, World!"), nil)
//	fmt.Println("CID:", result.CID)
//
//	// Get gateway URL
//	gateway := client.GetGatewayURL(result.CID, "")
//	fmt.Println("URL:", gateway.URL)
package storage

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	"time"
)

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

// Default endpoints.
const (
	DefaultEndpoint = "https://storage.synor.cc/api/v1"
	DefaultGateway  = "https://gateway.synor.cc"
)

// PinStatus represents the status of a pin.
type PinStatus string

const (
	Queued   PinStatus = "queued"
	Pinning  PinStatus = "pinning"
	Pinned   PinStatus = "pinned"
	Failed   PinStatus = "failed"
	Unpinned PinStatus = "unpinned"
)

// HashAlgorithm represents a hash algorithm.
type HashAlgorithm string

const (
	SHA256 HashAlgorithm = "sha2-256"
	BLAKE3 HashAlgorithm = "blake3"
)

// EntryType represents a directory entry type.
type EntryType string

const (
	FileType      EntryType = "file"
	DirectoryType EntryType = "directory"
)

// MatchType represents a pin matching type.
type MatchType string

const (
	Exact    MatchType = "exact"
	IExact   MatchType = "iexact"
	Partial  MatchType = "partial"
	IPartial MatchType = "ipartial"
)

// Config holds client configuration.
type Config struct {
	APIKey         string
	Endpoint       string
	Gateway        string
	PinningService string
	ChunkSize      int
	Timeout        time.Duration
	Debug          bool
}

// DefaultConfig returns a default configuration.
func DefaultConfig(apiKey string) Config {
	return Config{
		APIKey:    apiKey,
		Endpoint:  DefaultEndpoint,
		Gateway:   DefaultGateway,
		ChunkSize: 262144, // 256KB
		Timeout:   30 * time.Second,
		Debug:     false,
	}
}

// Client is the Synor Storage client.
type Client struct {
	config     Config
	httpClient *http.Client
}

// 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,
		},
	}
}

// UploadOptions contains options for uploading content.
type UploadOptions struct {
	Pin               bool
	WrapWithDirectory bool
	CIDVersion        int
	HashAlgorithm     HashAlgorithm
}

// UploadResponse contains the result of an upload.
type UploadResponse struct {
	CID  string `json:"cid"`
	Size int64  `json:"size"`
	Name string `json:"name,omitempty"`
	Hash string `json:"hash,omitempty"`
}

// DownloadOptions contains options for downloading content.
type DownloadOptions struct {
	Offset int64
	Length int64
}

// Pin represents pin information.
type Pin struct {
	CID       string    `json:"cid"`
	Status    PinStatus `json:"status"`
	Name      string    `json:"name,omitempty"`
	Size      int64     `json:"size,omitempty"`
	CreatedAt int64     `json:"createdAt,omitempty"`
	ExpiresAt int64     `json:"expiresAt,omitempty"`
	Delegates []string  `json:"delegates,omitempty"`
}

// PinRequest contains options for pinning content.
type PinRequest struct {
	CID      string   `json:"cid"`
	Name     string   `json:"name,omitempty"`
	Duration int64    `json:"duration,omitempty"`
	Origins  []string `json:"origins,omitempty"`
}

// ListPinsOptions contains options for listing pins.
type ListPinsOptions struct {
	Status []PinStatus
	Match  MatchType
	Name   string
	Limit  int
	Offset int
}

// ListPinsResponse contains the result of listing pins.
type ListPinsResponse struct {
	Pins    []Pin `json:"pins"`
	Total   int   `json:"total"`
	HasMore bool  `json:"hasMore"`
}

// GatewayURL represents a gateway URL.
type GatewayURL struct {
	URL  string
	CID  string
	Path string
}

// CarBlock represents a CAR block.
type CarBlock struct {
	CID  string `json:"cid"`
	Data string `json:"data"`
	Size int64  `json:"size,omitempty"`
}

// CarFile represents a CAR file.
type CarFile struct {
	Version int        `json:"version"`
	Roots   []string   `json:"roots"`
	Blocks  []CarBlock `json:"blocks,omitempty"`
	Size    int64      `json:"size,omitempty"`
}

// FileEntry represents a file entry for directory creation.
type FileEntry struct {
	Name    string
	Content []byte
	CID     string
}

// DirectoryEntry represents a directory entry.
type DirectoryEntry struct {
	Name string    `json:"name"`
	CID  string    `json:"cid"`
	Size int64     `json:"size,omitempty"`
	Type EntryType `json:"type"`
}

// ImportCarResponse contains the result of importing a CAR file.
type ImportCarResponse struct {
	Roots          []string `json:"roots"`
	BlocksImported int      `json:"blocksImported"`
}

// StorageStats represents storage statistics.
type StorageStats struct {
	TotalSize int64 `json:"totalSize"`
	PinCount  int   `json:"pinCount"`
	Bandwidth struct {
		Upload   int64 `json:"upload"`
		Download int64 `json:"download"`
	} `json:"bandwidth,omitempty"`
}

// Upload uploads content to storage.
func (c *Client) Upload(ctx context.Context, data []byte, opts *UploadOptions) (*UploadResponse, error) {
	var buf bytes.Buffer
	writer := multipart.NewWriter(&buf)

	part, err := writer.CreateFormFile("file", "file")
	if err != nil {
		return nil, fmt.Errorf("failed to create form file: %w", err)
	}

	if _, err := part.Write(data); err != nil {
		return nil, fmt.Errorf("failed to write data: %w", err)
	}

	if err := writer.Close(); err != nil {
		return nil, fmt.Errorf("failed to close writer: %w", err)
	}

	params := url.Values{}
	if opts != nil {
		if opts.Pin {
			params.Set("pin", "true")
		}
		if opts.WrapWithDirectory {
			params.Set("wrapWithDirectory", "true")
		}
		if opts.CIDVersion != 0 {
			params.Set("cidVersion", fmt.Sprintf("%d", opts.CIDVersion))
		}
		if opts.HashAlgorithm != "" {
			params.Set("hashAlgorithm", string(opts.HashAlgorithm))
		}
	}

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

	reqURL := c.config.Endpoint + path
	req, err := http.NewRequestWithContext(ctx, "POST", reqURL, &buf)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	req.Header.Set("Content-Type", writer.FormDataContentType())

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

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

	if resp.StatusCode >= 400 {
		return nil, c.parseError(resp)
	}

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

	return &result, nil
}

// Download downloads content by CID.
func (c *Client) Download(ctx context.Context, cid string, opts *DownloadOptions) ([]byte, error) {
	params := url.Values{}
	if opts != nil {
		if opts.Offset > 0 {
			params.Set("offset", fmt.Sprintf("%d", opts.Offset))
		}
		if opts.Length > 0 {
			params.Set("length", fmt.Sprintf("%d", opts.Length))
		}
	}

	path := fmt.Sprintf("/content/%s", cid)
	if len(params) > 0 {
		path += "?" + params.Encode()
	}

	reqURL := c.config.Endpoint + path
	req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)

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

	if resp.StatusCode >= 400 {
		return nil, c.parseError(resp)
	}

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	return data, nil
}

// DownloadReader returns a reader for downloading content.
func (c *Client) DownloadReader(ctx context.Context, cid string, opts *DownloadOptions) (io.ReadCloser, error) {
	params := url.Values{}
	if opts != nil {
		if opts.Offset > 0 {
			params.Set("offset", fmt.Sprintf("%d", opts.Offset))
		}
		if opts.Length > 0 {
			params.Set("length", fmt.Sprintf("%d", opts.Length))
		}
	}

	path := fmt.Sprintf("/content/%s/stream", cid)
	if len(params) > 0 {
		path += "?" + params.Encode()
	}

	reqURL := c.config.Endpoint + path
	req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)

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

	if resp.StatusCode >= 400 {
		defer resp.Body.Close()
		return nil, c.parseError(resp)
	}

	return resp.Body, nil
}

// Pin pins content by CID.
func (c *Client) Pin(ctx context.Context, req PinRequest) (*Pin, error) {
	var result Pin
	if err := c.request(ctx, "POST", "/pins", req, &result); err != nil {
		return nil, err
	}
	return &result, nil
}

// Unpin unpins content by CID.
func (c *Client) Unpin(ctx context.Context, cid string) error {
	return c.request(ctx, "DELETE", "/pins/"+cid, nil, nil)
}

// GetPinStatus gets the pin status for a CID.
func (c *Client) GetPinStatus(ctx context.Context, cid string) (*Pin, error) {
	var result Pin
	if err := c.request(ctx, "GET", "/pins/"+cid, nil, &result); err != nil {
		return nil, err
	}
	return &result, nil
}

// ListPins lists pins.
func (c *Client) ListPins(ctx context.Context, opts *ListPinsOptions) (*ListPinsResponse, error) {
	path := "/pins"
	if opts != nil {
		params := url.Values{}
		if len(opts.Status) > 0 {
			var statuses []string
			for _, s := range opts.Status {
				statuses = append(statuses, string(s))
			}
			params.Set("status", joinStrings(statuses, ","))
		}
		if opts.Match != "" {
			params.Set("match", string(opts.Match))
		}
		if opts.Name != "" {
			params.Set("name", opts.Name)
		}
		if opts.Limit > 0 {
			params.Set("limit", fmt.Sprintf("%d", opts.Limit))
		}
		if opts.Offset > 0 {
			params.Set("offset", fmt.Sprintf("%d", opts.Offset))
		}
		if len(params) > 0 {
			path += "?" + params.Encode()
		}
	}

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

// GetGatewayURL returns the gateway URL for content.
func (c *Client) GetGatewayURL(cid string, path string) GatewayURL {
	fullPath := "/" + cid
	if path != "" {
		fullPath += "/" + path
	}
	return GatewayURL{
		URL:  c.config.Gateway + "/ipfs" + fullPath,
		CID:  cid,
		Path: path,
	}
}

// CreateCar creates a CAR file from files.
func (c *Client) CreateCar(ctx context.Context, files []FileEntry) (*CarFile, error) {
	type fileData struct {
		Name    string `json:"name"`
		Content string `json:"content,omitempty"`
		CID     string `json:"cid,omitempty"`
	}

	var entries []fileData
	for _, f := range files {
		entry := fileData{Name: f.Name}
		if len(f.Content) > 0 {
			entry.Content = base64.StdEncoding.EncodeToString(f.Content)
		}
		if f.CID != "" {
			entry.CID = f.CID
		}
		entries = append(entries, entry)
	}

	var result CarFile
	if err := c.request(ctx, "POST", "/car/create", map[string]interface{}{"files": entries}, &result); err != nil {
		return nil, err
	}
	return &result, nil
}

// ImportCar imports a CAR file.
func (c *Client) ImportCar(ctx context.Context, carData []byte, pin bool) (*ImportCarResponse, error) {
	encoded := base64.StdEncoding.EncodeToString(carData)
	body := map[string]interface{}{
		"car": encoded,
		"pin": pin,
	}

	var result ImportCarResponse
	if err := c.request(ctx, "POST", "/car/import", body, &result); err != nil {
		return nil, err
	}
	return &result, nil
}

// ExportCar exports content as a CAR file.
func (c *Client) ExportCar(ctx context.Context, cid string) ([]byte, error) {
	reqURL := c.config.Endpoint + "/car/export/" + cid
	req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)

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

	if resp.StatusCode >= 400 {
		return nil, c.parseError(resp)
	}

	return io.ReadAll(resp.Body)
}

// CreateDirectory creates a directory from files.
func (c *Client) CreateDirectory(ctx context.Context, files []FileEntry) (*UploadResponse, error) {
	type fileData struct {
		Name    string `json:"name"`
		Content string `json:"content,omitempty"`
		CID     string `json:"cid,omitempty"`
	}

	var entries []fileData
	for _, f := range files {
		entry := fileData{Name: f.Name}
		if len(f.Content) > 0 {
			entry.Content = base64.StdEncoding.EncodeToString(f.Content)
		}
		if f.CID != "" {
			entry.CID = f.CID
		}
		entries = append(entries, entry)
	}

	var result UploadResponse
	if err := c.request(ctx, "POST", "/directory", map[string]interface{}{"files": entries}, &result); err != nil {
		return nil, err
	}
	return &result, nil
}

// ListDirectory lists directory contents.
func (c *Client) ListDirectory(ctx context.Context, cid string, path string) ([]DirectoryEntry, error) {
	apiPath := fmt.Sprintf("/directory/%s", cid)
	if path != "" {
		apiPath += "?path=" + url.QueryEscape(path)
	}

	var resp struct {
		Entries []DirectoryEntry `json:"entries"`
	}
	if err := c.request(ctx, "GET", apiPath, nil, &resp); err != nil {
		return nil, err
	}
	return resp.Entries, nil
}

// GetStats gets storage statistics.
func (c *Client) GetStats(ctx context.Context) (*StorageStats, error) {
	var result StorageStats
	if err := c.request(ctx, "GET", "/stats", nil, &result); err != nil {
		return nil, err
	}
	return &result, nil
}

// Exists checks if content exists.
func (c *Client) Exists(ctx context.Context, cid string) (bool, error) {
	reqURL := c.config.Endpoint + "/content/" + cid
	req, err := http.NewRequestWithContext(ctx, "HEAD", reqURL, nil)
	if err != nil {
		return false, err
	}

	req.Header.Set("Authorization", "Bearer "+c.config.APIKey)

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

	return resp.StatusCode == 200, nil
}

// GetMetadata gets content metadata.
func (c *Client) GetMetadata(ctx context.Context, cid string) (map[string]interface{}, error) {
	var result map[string]interface{}
	if err := c.request(ctx, "GET", "/content/"+cid+"/metadata", nil, &result); err != nil {
		return nil, err
	}
	return result, nil
}

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")

	if c.config.Debug {
		fmt.Printf("[SynorStorage] %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 {
		return c.parseError(resp)
	}

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

	return nil
}

func (c *Client) parseError(resp *http.Response) error {
	var errResp struct {
		Message string `json:"message"`
		Code    string `json:"code"`
	}
	json.NewDecoder(resp.Body).Decode(&errResp)
	return &StorageError{
		Message:    errResp.Message,
		StatusCode: resp.StatusCode,
		Code:       errResp.Code,
	}
}

func joinStrings(strs []string, sep string) string {
	if len(strs) == 0 {
		return ""
	}
	result := strs[0]
	for i := 1; i < len(strs); i++ {
		result += sep + strs[i]
	}
	return result
}

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

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