package satusehat

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

	"service/internal/infrastructure/config"
	"service/pkg/logger"
)

type client struct {
	cfg         config.SatuSehatConfig
	httpClient  *http.Client
	tokenData   map[string]interface{}
	tokenExpiry time.Time
	mu          sync.RWMutex
}

// NewSatuSehatClient membuat instance baru dari SatuSehatClient.
func NewSatuSehatClient(cfg config.SatuSehatConfig) SatuSehatClient {
	return &client{
		cfg:        cfg,
		httpClient: &http.Client{Timeout: cfg.Timeout},
	}
}

// GetAccessToken mengelola request token OAuth2 (Client Credentials) dan melakukan caching di memori.
func (c *client) GetAccessToken(ctx context.Context) (map[string]interface{}, error) {
	c.mu.RLock()
	// Cek apakah token masih valid (dengan buffer 1 menit untuk mencegah kegagalan)
	if c.tokenData != nil && time.Now().Before(c.tokenExpiry) {
		data := c.tokenData
		c.mu.RUnlock()
		return data, nil
	}
	c.mu.RUnlock()

	c.mu.Lock()
	defer c.mu.Unlock()

	// Double check setelah mendapat lock
	if c.tokenData != nil && time.Now().Before(c.tokenExpiry) {
		return c.tokenData, nil
	}

	data := url.Values{}
	data.Set("client_id", c.cfg.ClientID)
	data.Set("client_secret", c.cfg.ClientSecret)
	// Sesuai standar OAuth2, grant_type disisipkan dalam body request
	data.Set("grant_type", "client_credentials")

	// Deteksi otomatis jika Auth URL di environment hanya berupa Base (tanpa /accesstoken)
	authURL := strings.TrimRight(c.cfg.AuthURL, "/")
	if !strings.HasSuffix(authURL, "accesstoken") {
		authURL += "/accesstoken"
	}
	// Kemenkes juga sering mensyaratkan query parameter grant_type di URL
	if !strings.Contains(authURL, "grant_type") {
		authURL += "?grant_type=client_credentials"
	}

	req, err := http.NewRequestWithContext(ctx, "POST", authURL, strings.NewReader(data.Encode()))
	if err != nil {
		return nil, fmt.Errorf("failed to create auth request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

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

	// Baca terlebih dahulu seluruh response body untuk memudahkan debugging jika gagal parse JSON
	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read auth response body: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		logger.Default().Error("SatuSehat Auth Error", logger.Int("status", resp.StatusCode), logger.String("response", string(respBody)))
		return nil, fmt.Errorf("SatuSehat Auth failed with status %d: %s", resp.StatusCode, string(respBody))
	}

	var result map[string]interface{}

	if err := json.Unmarshal(respBody, &result); err != nil {
		logger.Default().Error("SatuSehat Auth Decode Error", logger.String("response", string(respBody)), logger.ErrorField(err))
		return nil, fmt.Errorf("failed to decode auth response: %w", err)
	}

	if _, ok := result["access_token"].(string); !ok {
		return nil, fmt.Errorf("access_token missing or invalid in response: %s", string(respBody))
	}

	c.tokenData = result

	var expiresIn int
	if expRaw, ok := result["expires_in"]; ok {
		switch v := expRaw.(type) {
		case string:
			expiresIn, _ = strconv.Atoi(v)
		case float64: // Pada interface{}, JSON number dibaca sebagai float64 oleh Go
			expiresIn = int(v)
		default:
			expiresIn = 3599 // Fallback default 1 jam jika atribut tidak ada/tidak sesuai
		}
	} else {
		expiresIn = 3599
	}

	// Simpan masa berlaku token dikurangi 60 detik sebagai safety margin
	c.tokenExpiry = time.Now().Add(time.Duration(expiresIn-60) * time.Second)

	logger.Default().Info("🔑 SatuSehat Access Token refreshed successfully")

	return c.tokenData, nil
}

// RefreshToken memaksa pengambilan token baru (melewati cache).
func (c *client) RefreshToken(ctx context.Context) (map[string]interface{}, error) {
	c.mu.Lock()
	c.tokenData = nil // Invalidasi cache token
	c.mu.Unlock()
	return c.GetAccessToken(ctx)
}

// do merupakan helper HTTP request secara general ke berbagai Base URL SatuSehat
func (c *client) do(ctx context.Context, baseURL, method, endpoint string, body interface{}) ([]byte, error) {
	if !c.cfg.Enabled {
		return nil, fmt.Errorf("SatuSehat integration is disabled in configuration")
	}

	authData, err := c.GetAccessToken(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get SatuSehat access token: %w", err)
	}
	token, ok := authData["access_token"].(string)
	if !ok {
		return nil, fmt.Errorf("access token is missing in auth data")
	}

	baseURL = strings.TrimRight(baseURL, "/")
	endpointPath := strings.TrimLeft(endpoint, "/")
	targetURL := fmt.Sprintf("%s/%s", baseURL, endpointPath)

	// Marshal body terlebih dahulu agar bisa digunakan berulang kali jika butuh retry
	var jsonBody []byte
	if body != nil {
		var err error
		jsonBody, err = json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal request body: %w", err)
		}
	}

	// Helper function untuk merakit ulang request (sangat berguna untuk skenario Retry)
	buildReq := func(accessToken string) (*http.Request, error) {
		var bodyReader io.Reader
		if jsonBody != nil {
			bodyReader = bytes.NewBuffer(jsonBody)
		}
		req, err := http.NewRequestWithContext(ctx, method, targetURL, bodyReader)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+accessToken)
		req.Header.Set("Accept", "application/json")
		if jsonBody != nil {
			if method == "PATCH" {
				req.Header.Set("Content-Type", "application/json-patch+json")
			} else {
				req.Header.Set("Content-Type", "application/json")
			}
		}
		return req, nil
	}

	req, err := buildReq(token)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	// [RADAR] Logging Outgoing Request
	logger.Default().Info("🛫 SatuSehat API Request Sent",
		logger.String("target_url", targetURL),
		logger.String("method", method),
	)

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

	// [AUTO-RETRY LOGIC] Jika Kemenkes menjawab 401 Unauthorized, token mungkin expired/revoked
	if resp.StatusCode == http.StatusUnauthorized {
		// Tutup body request pertama yang gagal agar tidak memory leak
		resp.Body.Close()

		logger.Default().Warn("🔄 Token Satu Sehat ditolak (401). Melakukan Auto-Refresh dan mengulang request...")

		// 1. Refresh Token secara paksa
		authData, err = c.RefreshToken(ctx)
		if err != nil {
			return nil, fmt.Errorf("failed to auto-refresh token for retry: %w", err)
		}
		token, ok = authData["access_token"].(string)
		if !ok {
			return nil, fmt.Errorf("access token is missing after auto-refresh")
		}

		// 2. Bangun ulang Request dengan token terbaru
		req, err = buildReq(token)
		if err != nil {
			return nil, fmt.Errorf("failed to rebuild retry request: %w", err)
		}

		// 3. Tembak ulang API Kemenkes
		resp, err = c.httpClient.Do(req)
		if err != nil {
			return nil, fmt.Errorf("http retry request failed: %w", err)
		}
	}

	// Pastikan body response dari eksekusi terakhir (berhasil atau retry) selalu ditutup
	defer resp.Body.Close()

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

	// Tangani response status code error (seperti FHIROperationOutcome)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		logger.Default().Error("SatuSehat API Error",
			logger.Int("status_code", resp.StatusCode),
			logger.String("response", string(respBody)),
		)

		// Coba parse response body sebagai FHIR OperationOutcome untuk error yang lebih terbaca
		var outcome map[string]interface{}
		if err := json.Unmarshal(respBody, &outcome); err == nil && outcome["resourceType"] == "OperationOutcome" {
			return nil, &ErrorOperationOutcome{
				StatusCode: resp.StatusCode,
				Outcome:    outcome,
			}
		}

		return nil, fmt.Errorf("SatuSehat API Error (HTTP %d): %s", resp.StatusCode, string(respBody))
	}

	return respBody, nil
}

func (c *client) DoRequest(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error) {
	return c.do(ctx, c.cfg.BaseURL, method, endpoint, body)
}

func (c *client) DoKFA(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error) {
	return c.do(ctx, c.cfg.KFAURL, method, endpoint, body)
}

func (c *client) DoConsent(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error) {
	return c.do(ctx, c.cfg.ConsentURL, method, endpoint, body)
}
