// Package client provides a Go SDK for interacting with the MapleFile API.
package client

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

// Logger is an interface for logging API requests.
// This allows the client to work with any logging library (zap, logrus, etc.)
type Logger interface {
	// Debug logs a debug message with optional key-value pairs
	Debug(msg string, keysAndValues ...interface{})
	// Info logs an info message with optional key-value pairs
	Info(msg string, keysAndValues ...interface{})
	// Warn logs a warning message with optional key-value pairs
	Warn(msg string, keysAndValues ...interface{})
	// Error logs an error message with optional key-value pairs
	Error(msg string, keysAndValues ...interface{})
}

// Client is the MapleFile API client.
type Client struct {
	baseURL    string
	httpClient *http.Client
	logger     Logger

	// Token storage with mutex for thread safety
	mu           sync.RWMutex
	accessToken  string
	refreshToken string

	// Callback when tokens are refreshed
	// Parameters: accessToken, refreshToken, accessTokenExpiryDate (RFC3339 format)
	onTokenRefresh func(accessToken, refreshToken, accessTokenExpiryDate string)

	// Flag to prevent recursive token refresh (atomic for lock-free reads)
	isRefreshing atomic.Bool
}

// Predefined environment URLs
const (
	// ProductionURL is the production API endpoint
	ProductionURL = "https://maplefile.ca"

	// LocalURL is the default local development API endpoint
	LocalURL = "http://localhost:8000"
)

// Config holds the configuration for creating a new Client.
type Config struct {
	// BaseURL is the base URL of the MapleFile API (e.g., "https://maplefile.ca")
	// You can use predefined constants: ProductionURL or LocalURL
	BaseURL string

	// HTTPClient is an optional custom HTTP client. If nil, a default client with 30s timeout is used.
	HTTPClient *http.Client

	// Logger is an optional logger for API request logging. If nil, no logging is performed.
	Logger Logger
}

// New creates a new MapleFile API client with the given configuration.
//
// Security Note: This client uses Go's standard http.Client without certificate
// pinning. This is intentional and secure because:
//
//  1. TLS termination is handled by a reverse proxy (Caddy/Nginx) in production,
//     which manages certificates via Let's Encrypt with automatic renewal.
//  2. Go's default TLS configuration already validates certificate chains,
//     expiration, and hostname matching against system CA roots.
//  3. The application uses end-to-end encryption (E2EE) - even if TLS were
//     compromised, attackers would only see encrypted data they cannot decrypt.
//  4. Certificate pinning would require app updates every 90 days (Let's Encrypt
//     rotation) or risk bricking deployed applications.
//
// See: docs/OWASP_AUDIT_REPORT.md (Finding 4.1) for full security analysis.
func New(cfg Config) *Client {
	httpClient := cfg.HTTPClient
	if httpClient == nil {
		// Standard HTTP client with timeout. Certificate pinning is intentionally
		// not implemented - see security note above.
		httpClient = &http.Client{
			Timeout: 30 * time.Second,
		}
	}

	// Ensure baseURL doesn't have trailing slash
	baseURL := strings.TrimSuffix(cfg.BaseURL, "/")

	return &Client{
		baseURL:    baseURL,
		httpClient: httpClient,
		logger:     cfg.Logger,
	}
}

// NewProduction creates a new MapleFile API client configured for production.
func NewProduction() *Client {
	return New(Config{BaseURL: ProductionURL})
}

// NewLocal creates a new MapleFile API client configured for local development.
func NewLocal() *Client {
	return New(Config{BaseURL: LocalURL})
}

// NewWithURL creates a new MapleFile API client with a custom URL.
func NewWithURL(baseURL string) *Client {
	return New(Config{BaseURL: baseURL})
}

// SetTokens sets the access and refresh tokens for authentication.
func (c *Client) SetTokens(accessToken, refreshToken string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.accessToken = accessToken
	c.refreshToken = refreshToken
}

// GetTokens returns the current access and refresh tokens.
func (c *Client) GetTokens() (accessToken, refreshToken string) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.accessToken, c.refreshToken
}

// OnTokenRefresh sets a callback function that will be called when tokens are refreshed.
// This is useful for persisting the new tokens to storage.
// The callback receives: accessToken, refreshToken, and accessTokenExpiryDate (RFC3339 format).
func (c *Client) OnTokenRefresh(callback func(accessToken, refreshToken, accessTokenExpiryDate string)) {
	c.onTokenRefresh = callback
}

// SetBaseURL changes the base URL of the API.
// This is useful for switching between environments at runtime.
func (c *Client) SetBaseURL(baseURL string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.baseURL = strings.TrimSuffix(baseURL, "/")
}

// GetBaseURL returns the current base URL.
func (c *Client) GetBaseURL() string {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.baseURL
}

// Health checks if the API is healthy.
func (c *Client) Health(ctx context.Context) (*HealthResponse, error) {
	var resp HealthResponse
	if err := c.doRequest(ctx, "GET", "/health", nil, &resp, false); err != nil {
		return nil, err
	}
	return &resp, nil
}

// Version returns the API version information.
func (c *Client) Version(ctx context.Context) (*VersionResponse, error) {
	var resp VersionResponse
	if err := c.doRequest(ctx, "GET", "/version", nil, &resp, false); err != nil {
		return nil, err
	}
	return &resp, nil
}

// doRequest performs an HTTP request with automatic token refresh on 401.
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}, requiresAuth bool) error {
	return c.doRequestWithRetry(ctx, method, path, body, result, requiresAuth, true)
}

// doRequestWithRetry performs an HTTP request with optional retry on 401.
func (c *Client) doRequestWithRetry(ctx context.Context, method, path string, body interface{}, result interface{}, requiresAuth bool, allowRetry bool) error {
	// Build URL
	url := c.baseURL + path

	// Log API request
	if c.logger != nil {
		c.logger.Info("API request", "method", method, "url", url)
	}

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

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

	// Set headers
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	// Accept both standard JSON and RFC 9457 problem+json responses
	req.Header.Set("Accept", "application/json, application/problem+json")

	// Add authorization header if required
	if requiresAuth {
		c.mu.RLock()
		token := c.accessToken
		c.mu.RUnlock()

		if token == "" {
			return &APIError{
				ProblemDetail: ProblemDetail{
					Status: 401,
					Title:  "Unauthorized",
					Detail: "No access token available",
				},
			}
		}
		req.Header.Set("Authorization", fmt.Sprintf("JWT %s", token))
	}

	// Execute request
	resp, err := c.httpClient.Do(req)
	if err != nil {
		if c.logger != nil {
			c.logger.Error("API request failed", "method", method, "url", url, "error", err.Error())
		}
		return fmt.Errorf("failed to execute request: %w", err)
	}
	defer resp.Body.Close()

	// Log API response
	if c.logger != nil {
		c.logger.Info("API response", "method", method, "url", url, "status", resp.StatusCode)
	}

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

	// Handle 401 with automatic token refresh
	// Use atomic.Bool for lock-free check to avoid unnecessary lock acquisition
	if resp.StatusCode == http.StatusUnauthorized && requiresAuth && allowRetry && !c.isRefreshing.Load() {
		c.mu.Lock()
		// Double-check under lock and verify refresh token exists
		if c.refreshToken != "" && !c.isRefreshing.Load() {
			c.isRefreshing.Store(true)
			c.mu.Unlock()

			// Attempt to refresh token
			refreshErr := c.refreshAccessToken(ctx)
			c.isRefreshing.Store(false)

			if refreshErr == nil {
				// Retry the original request without allowing another retry
				return c.doRequestWithRetry(ctx, method, path, body, result, requiresAuth, false)
			}
		} else {
			c.mu.Unlock()
		}
	}

	// Handle error status codes
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return parseErrorResponse(respBody, resp.StatusCode)
	}

	// Parse successful response
	if result != nil && len(respBody) > 0 {
		if err := json.Unmarshal(respBody, result); err != nil {
			return fmt.Errorf("failed to parse response: %w", err)
		}
	}

	return nil
}

// refreshAccessToken attempts to refresh the access token using the refresh token.
func (c *Client) refreshAccessToken(ctx context.Context) error {
	c.mu.RLock()
	refreshToken := c.refreshToken
	c.mu.RUnlock()

	if refreshToken == "" {
		return fmt.Errorf("no refresh token available")
	}

	// Build refresh request
	url := c.baseURL + "/api/v1/token/refresh"

	reqBody := map[string]string{
		"value": refreshToken,  // Backend expects "value" field, not "refresh_token"
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return err
	}

	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData))
	if err != nil {
		return err
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	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 != http.StatusOK {
		return parseErrorResponse(respBody, resp.StatusCode)
	}

	// Parse the refresh response
	// Note: Backend returns access_token_expiry_date and refresh_token_expiry_date,
	// but the callback currently only passes tokens. Expiry dates are available
	// in the LoginResponse type if needed for future enhancements.
	var tokenResp struct {
		AccessToken            string `json:"access_token"`
		RefreshToken           string `json:"refresh_token"`
		AccessTokenExpiryDate  string `json:"access_token_expiry_date"`
		RefreshTokenExpiryDate string `json:"refresh_token_expiry_date"`
	}
	if err := json.Unmarshal(respBody, &tokenResp); err != nil {
		return err
	}

	// Update stored tokens
	c.mu.Lock()
	c.accessToken = tokenResp.AccessToken
	c.refreshToken = tokenResp.RefreshToken
	c.mu.Unlock()

	// Notify callback if set, passing the expiry date so callers can track actual expiration
	if c.onTokenRefresh != nil {
		c.onTokenRefresh(tokenResp.AccessToken, tokenResp.RefreshToken, tokenResp.AccessTokenExpiryDate)
	}

	return nil
}

// doRequestRaw performs an HTTP request and returns the raw response body.
// This is useful for endpoints that return non-JSON responses.
func (c *Client) doRequestRaw(ctx context.Context, method, path string, body interface{}, requiresAuth bool) ([]byte, error) {
	return c.doRequestRawWithRetry(ctx, method, path, body, requiresAuth, true)
}

// doRequestRawWithRetry performs an HTTP request with optional retry on 401.
func (c *Client) doRequestRawWithRetry(ctx context.Context, method, path string, body interface{}, requiresAuth bool, allowRetry bool) ([]byte, error) {
	// Build URL
	url := c.baseURL + path

	// Log API request
	if c.logger != nil {
		c.logger.Info("API request", "method", method, "url", url)
	}

	// Prepare request body - we need to be able to re-read it for retry
	var bodyData []byte
	if body != nil {
		var err error
		bodyData, err = json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal request body: %w", err)
		}
	}

	// Create request
	var bodyReader io.Reader
	if bodyData != nil {
		bodyReader = bytes.NewReader(bodyData)
	}
	req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

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

	// Add authorization header if required
	if requiresAuth {
		c.mu.RLock()
		token := c.accessToken
		c.mu.RUnlock()

		if token == "" {
			return nil, &APIError{
				ProblemDetail: ProblemDetail{
					Status: 401,
					Title:  "Unauthorized",
					Detail: "No access token available",
				},
			}
		}
		req.Header.Set("Authorization", fmt.Sprintf("JWT %s", token))
	}

	// Execute request
	resp, err := c.httpClient.Do(req)
	if err != nil {
		if c.logger != nil {
			c.logger.Error("API request failed", "method", method, "url", url, "error", err.Error())
		}
		return nil, fmt.Errorf("failed to execute request: %w", err)
	}
	defer resp.Body.Close()

	// Log API response
	if c.logger != nil {
		c.logger.Info("API response", "method", method, "url", url, "status", resp.StatusCode)
	}

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

	// Handle 401 with automatic token refresh
	if resp.StatusCode == http.StatusUnauthorized && requiresAuth && allowRetry && !c.isRefreshing.Load() {
		c.mu.Lock()
		if c.refreshToken != "" && !c.isRefreshing.Load() {
			c.isRefreshing.Store(true)
			c.mu.Unlock()

			// Attempt to refresh token
			refreshErr := c.refreshAccessToken(ctx)
			c.isRefreshing.Store(false)

			if refreshErr == nil {
				// Retry the original request without allowing another retry
				return c.doRequestRawWithRetry(ctx, method, path, body, requiresAuth, false)
			}
		} else {
			c.mu.Unlock()
		}
	}

	// Handle error status codes
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, parseErrorResponse(respBody, resp.StatusCode)
	}

	return respBody, nil
}
