// SPDX-License-Identifier: GPL-3.0-or-later

// Package server provides an HTTP client for communicating with the Private Archive server.
// It handles mTLS authentication and routes all traffic through Tor.
package server

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/privatebackup/client-backend/internal/keyfile"
)

// Client is the server communication client.
type Client struct {
	httpClient  *http.Client
	baseURL     string
	certHash    string
	keyFile     *keyfile.BackupKeyFile
	maxConns    int
	timeout     time.Duration
	dialer      Dialer
	mu          sync.RWMutex
	connected   bool
	lastHealthy time.Time
}

// Dialer is an interface for creating network connections (for Tor integration).
type Dialer interface {
	DialContext(ctx context.Context, network, address string) (net.Conn, error)
}

// Config holds client configuration.
type Config struct {
	// ServerAddress is the .onion address of the backup server
	ServerAddress string

	// KeyFile contains the client credentials
	KeyFile *keyfile.BackupKeyFile

	// Dialer is the network dialer (typically Tor)
	Dialer Dialer

	// MaxConnections limits concurrent connections
	MaxConnections int

	// Timeout for requests
	Timeout time.Duration
}

// DefaultConfig returns default client configuration.
func DefaultConfig() *Config {
	return &Config{
		MaxConnections: 1,
		Timeout:        30 * time.Minute,
	}
}

// NewClient creates a new server client.
func NewClient(cfg *Config) (*Client, error) {
	if cfg == nil {
		cfg = DefaultConfig()
	}

	if cfg.ServerAddress == "" {
		return nil, fmt.Errorf("server address is required")
	}
	if cfg.KeyFile == nil {
		return nil, fmt.Errorf("key file is required")
	}
	if cfg.Dialer == nil {
		return nil, fmt.Errorf("dialer is required")
	}

	// Create TLS config with client certificate
	tlsConfig, err := createTLSConfig(cfg.KeyFile, cfg.ServerAddress)
	if err != nil {
		return nil, fmt.Errorf("failed to create TLS config: %w", err)
	}

	// Create HTTP transport with connection limits
	transport := &http.Transport{
		DialContext:           cfg.Dialer.DialContext,
		TLSClientConfig:       tlsConfig,
		MaxIdleConns:          cfg.MaxConnections,
		MaxConnsPerHost:       cfg.MaxConnections,
		IdleConnTimeout:       90 * time.Second,
		ResponseHeaderTimeout: 5 * time.Minute,
		// Disable HTTP/2 for better Tor compatibility
		ForceAttemptHTTP2: false,
	}

	httpClient := &http.Client{
		Transport: transport,
		Timeout:   cfg.Timeout,
	}

	return &Client{
		httpClient: httpClient,
		baseURL:    fmt.Sprintf("https://%s", cfg.ServerAddress),
		certHash:   cfg.KeyFile.CertHash,
		keyFile:    cfg.KeyFile,
		maxConns:   cfg.MaxConnections,
		timeout:    cfg.Timeout,
		dialer:     cfg.Dialer,
	}, nil
}

// createTLSConfig creates a TLS configuration with the client certificate.
// SECURITY NOTE: InsecureSkipVerify is ONLY set for .onion addresses because:
// 1. Tor hidden services (.onion) provide cryptographic identity via their address
// 2. The .onion address IS the server's public key hash - no CA needed
// 3. mTLS client certificate still authenticates us to the server
// 4. All traffic is end-to-end encrypted by Tor
// For non-.onion addresses, standard TLS verification is enforced.
func createTLSConfig(kf *keyfile.BackupKeyFile, serverAddress string) (*tls.Config, error) {
	// Create certificate for TLS
	certPEM, keyPEM, err := encodeToPEM(kf)
	if err != nil {
		return nil, err
	}

	cert, err := tls.X509KeyPair(certPEM, keyPEM)
	if err != nil {
		return nil, fmt.Errorf("failed to load key pair: %w", err)
	}

	// Only skip server cert verification for .onion addresses
	// (Tor hidden services provide identity via their address)
	isOnion := isOnionAddress(serverAddress)

	return &tls.Config{
		Certificates:       []tls.Certificate{cert},
		InsecureSkipVerify: isOnion,
		MinVersion:         tls.VersionTLS13,
		// TLS 1.3 automatically selects strong cipher suites
	}, nil
}

// isOnionAddress checks if an address is a Tor .onion hidden service.
func isOnionAddress(addr string) bool {
	// Strip port if present
	host := addr
	if idx := strings.LastIndex(addr, ":"); idx != -1 {
		host = addr[:idx]
	}
	return strings.HasSuffix(strings.ToLower(host), ".onion")
}

// encodeToPEM encodes the key file components to PEM format.
func encodeToPEM(kf *keyfile.BackupKeyFile) (certPEM, keyPEM []byte, err error) {
	certRaw, key, err := kf.TLSCertificate()
	if err != nil {
		return nil, nil, err
	}

	// Encode certificate
	certPEM = pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: certRaw,
	})

	// Encode private key
	ecKey, ok := key.(*ecdsa.PrivateKey)
	if !ok {
		return nil, nil, fmt.Errorf("private key is not ECDSA")
	}

	keyBytes, err := x509.MarshalECPrivateKey(ecKey)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to marshal private key: %w", err)
	}
	keyPEM = pem.EncodeToMemory(&pem.Block{
		Type:  "EC PRIVATE KEY",
		Bytes: keyBytes,
	})

	return certPEM, keyPEM, nil
}

// CertHash returns the client's certificate hash (identity).
func (c *Client) CertHash() string {
	return c.certHash
}

// BaseURL returns the server's base URL.
func (c *Client) BaseURL() string {
	return c.baseURL
}

// HealthCheck performs a health check against the server.
func (c *Client) HealthCheck(ctx context.Context) error {
	resp, err := c.Get(ctx, "/health")
	if err != nil {
		return fmt.Errorf("health check failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("health check returned status %d", resp.StatusCode)
	}

	c.mu.Lock()
	c.connected = true
	c.lastHealthy = time.Now()
	c.mu.Unlock()

	return nil
}

// IsHealthy returns whether the last health check was successful.
func (c *Client) IsHealthy() bool {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.connected
}

// LastHealthy returns when the server was last known to be healthy.
func (c *Client) LastHealthy() time.Time {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.lastHealthy
}

// Get performs an HTTP GET request.
func (c *Client) Get(ctx context.Context, path string) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
	if err != nil {
		return nil, err
	}
	return c.httpClient.Do(req)
}

// Post performs an HTTP POST request with JSON body.
func (c *Client) Post(ctx context.Context, path string, body interface{}) (*http.Response, error) {
	return c.doJSON(ctx, http.MethodPost, path, body)
}

// Put performs an HTTP PUT request with JSON body.
func (c *Client) Put(ctx context.Context, path string, body interface{}) (*http.Response, error) {
	return c.doJSON(ctx, http.MethodPut, path, body)
}

// Delete performs an HTTP DELETE request.
func (c *Client) Delete(ctx context.Context, path string) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, c.baseURL+path, nil)
	if err != nil {
		return nil, err
	}
	return c.httpClient.Do(req)
}

// doJSON performs an HTTP request with a JSON body.
func (c *Client) doJSON(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
	var bodyReader io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal request body: %w", err)
		}
		bodyReader = bytes.NewReader(data)
	}

	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
	if err != nil {
		return nil, err
	}

	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	return c.httpClient.Do(req)
}

// DoRaw performs a raw HTTP request with an io.Reader body.
// This is useful for streaming large uploads.
func (c *Client) DoRaw(ctx context.Context, method, path string, body io.Reader, contentType string) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
	if err != nil {
		return nil, err
	}

	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}

	return c.httpClient.Do(req)
}

// Close closes the client and releases resources.
func (c *Client) Close() error {
	c.httpClient.CloseIdleConnections()
	return nil
}

// AccountStatus represents the account status response.
// Field names match the server's AccountStatusResponse JSON.
type AccountStatus struct {
	CertHash         string     `json:"certificate_hash"`
	Status           string     `json:"account_status"`
	CreditsRemaining float64    `json:"credits"`
	StorageBytes     int64      `json:"storage_bytes"`
	TotalStorageGB   float64    `json:"total_storage_gb"`
	MonthlyBurnRate  float64    `json:"monthly_burn_rate"`
	LastBackupAt     *time.Time `json:"last_backup_at,omitempty"`
	LastPaymentAt    *time.Time `json:"last_payment_at,omitempty"`
	EstimatedExpiry  *time.Time `json:"estimated_expiry,omitempty"`
	CreatedAt        time.Time  `json:"created_at"`
}

// StorageUsedBytes returns total storage bytes.
func (a *AccountStatus) StorageUsedBytes() int64 {
	return a.StorageBytes
}

// GetAccountStatus retrieves the current account status.
func (c *Client) GetAccountStatus(ctx context.Context) (*AccountStatus, error) {
	resp, err := c.Get(ctx, "/api/v1/account/status")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

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

	return &status, nil
}

// RegisterRequest is the request body for account registration.
type RegisterRequest struct {
	CSR string `json:"csr"`
}

// RegisterResponse is the response from account registration.
type RegisterResponse struct {
	Certificate      string         `json:"certificate"`
	CACertificate    string         `json:"ca_certificate"`
	CertHash         string         `json:"cert_hash"`
	MoneroSubaddress string         `json:"monero_subaddress"`
	Credits          float64        `json:"credits"`
	AccountStatus    string         `json:"account_status"`
	S3Credentials    *S3Credentials `json:"s3_credentials,omitempty"`
}

// S3Credentials contains the S3 access credentials returned by the server.
type S3Credentials struct {
	AccessKeyID     string `json:"access_key_id"`
	SecretAccessKey string `json:"secret_access_key"`
	BucketName      string `json:"key_prefix"`
	Endpoint        string `json:"endpoint"`
}

// Register registers a new client account with the server.
// This should be called without mTLS (before we have a certificate).
// Optional insecureSkipVerify overrides the .onion auto-detection (for testing only).
func Register(ctx context.Context, serverAddress string, csr []byte, dialer Dialer, insecureSkipVerify ...bool) (*RegisterResponse, error) {
	skipVerify := isOnionAddress(serverAddress)
	if len(insecureSkipVerify) > 0 {
		skipVerify = insecureSkipVerify[0]
	}

	// Create HTTP client without client certificate
	transport := &http.Transport{
		DialContext: dialer.DialContext,
		TLSClientConfig: &tls.Config{
			MinVersion:         tls.VersionTLS13,
			InsecureSkipVerify: skipVerify,
		},
		MaxIdleConns:    1,
		MaxConnsPerHost: 1,
	}

	httpClient := &http.Client{
		Transport: transport,
		Timeout:   5 * time.Minute,
	}
	defer httpClient.CloseIdleConnections()

	reqBody := RegisterRequest{
		CSR: string(csr),
	}

	data, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %w", err)
	}

	url := fmt.Sprintf("https://%s/api/v1/register", serverAddress)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(data))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")

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

	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("registration failed with status %d: %s", resp.StatusCode, string(body))
	}

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

	return &registerResp, nil
}

// PaymentIntent represents a payment intent for purchasing credits.
type PaymentIntent struct {
	IntentID         string    `json:"intent_id"`
	CreditsRequested float64   `json:"credits_requested"`
	AmountXMR        string    `json:"amount_xmr"`
	PaymentAddress   string    `json:"payment_address"`
	ExpiresAt        time.Time `json:"expires_at"`
	RateLocked       bool      `json:"rate_locked"`
}

// CreatePaymentIntent creates a payment intent for the specified credits.
func (c *Client) CreatePaymentIntent(ctx context.Context, credits float64) (*PaymentIntent, error) {
	body := map[string]float64{"credits_requested": credits}

	resp, err := c.Post(ctx, "/api/v1/account/payment/intent", body)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(bodyBytes))
	}

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

	return &intent, nil
}

// LTCPaymentIntent is a rate-locked LTC payment intent: a per-client receive
// address plus the locked LTC/ZAR rate and credit price. LTC follows the
// rate-lock model (not a fixed amount) — the caller computes the LTC needed for
// N credits as N * credit_price_zar / ltc_zar_rate_locked; any amount sent is
// credited at the locked rate while in-window, remainder carried forward.
type LTCPaymentIntent struct {
	IntentID          string    `json:"intent_id"`
	PaymentAddress    string    `json:"payment_address"`
	LTCZARRate        string    `json:"ltc_zar_rate_locked"`
	CreditPriceZAR    string    `json:"credit_price_zar"`
	CreditsRequested  int       `json:"credits_requested"`
	ExpectedAmountLTC string    `json:"expected_amount_ltc"` // exact LTC to send for CreditsRequested
	ExpiresAt         time.Time `json:"expires_at"`
}

// CreateLTCPaymentIntent issues a per-client LTC receive address with a locked
// LTC/ZAR rate (POST /api/v1/account/payment/ltc/intent; no request body).
func (c *Client) CreateLTCPaymentIntent(ctx context.Context, credits int) (*LTCPaymentIntent, error) {
	resp, err := c.Post(ctx, "/api/v1/account/payment/ltc/intent", map[string]int{"credits": credits})
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(bodyBytes))
	}

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

	return &intent, nil
}

// LTCPaymentStatus reports how far a client's pending LTC intent has been paid,
// so the UI can show an underpayment shortfall ("you've paid X of Y — send Z more").
type LTCPaymentStatus struct {
	HasPending        bool   `json:"has_pending"`
	IntentID          string `json:"intent_id"`
	CreditsRequested  int    `json:"credits_requested"`
	ExpectedAmountLTC string `json:"expected_amount_ltc"`
	ReceivedAmountLTC string `json:"received_amount_ltc"`
	RemainingLTC      string `json:"remaining_amount_ltc"`
	RemainingUSD      string `json:"remaining_amount_usd"`
	Status            string `json:"status"` // "awaiting" | "underpaid" | "sufficient"
}

// GetLTCPaymentStatus fetches the pending LTC intent's fulfillment
// (GET /api/v1/account/payment/ltc/status).
func (c *Client) GetLTCPaymentStatus(ctx context.Context) (*LTCPaymentStatus, error) {
	resp, err := c.Get(ctx, "/api/v1/account/payment/ltc/status")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(bodyBytes))
	}

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

	return &status, nil
}

// S3CredentialRotationResponse is the response from credential rotation.
type S3CredentialRotationResponse struct {
	AccessKeyID     string `json:"access_key_id"`
	SecretAccessKey string `json:"secret_access_key"`
	BucketName      string `json:"key_prefix"`
	RotatedAt       string `json:"rotated_at"`
}

// RotateS3Credentials requests rotation of the S3 secret access key.
// Only the secret key changes; the access key ID and bucket name are immutable.
func (c *Client) RotateS3Credentials(ctx context.Context) (*S3CredentialRotationResponse, error) {
	resp, err := c.Post(ctx, "/api/v1/account/credentials/rotate", nil)
	if err != nil {
		return nil, fmt.Errorf("credential rotation request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("credential rotation failed with status %d: %s", resp.StatusCode, string(body))
	}

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

	return &rotateResp, nil
}
