package services

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

	"api-service/internal/config"
	"api-service/pkg/logger"

	"github.com/mashingan/smapping"
	"github.com/tidwall/gjson"
)

// SatuSehatService interface for SATUSEHAT operations
type SatuSehatService interface {
	// Standard HTTP methods
	Get(ctx context.Context, endpoint string, result interface{}) error
	Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error
	Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error
	Delete(ctx context.Context, endpoint string, result interface{}) error

	// Raw response methods
	GetRawResponse(ctx context.Context, endpoint string) (*SatuSehatResponDTO, error)
	PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*SatuSehatResponDTO, error)

	// FHIR specific methods
	PostBundle(ctx context.Context, bundle interface{}) (*SatuSehatResponDTO, error)
	GetPatientByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error)
	GetPractitionerByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error)
	GetResourceByID(ctx context.Context, resourceType, id string) (*SatuSehatResponDTO, error)

	// Token management
	RefreshToken(ctx context.Context) error
	IsTokenValid() bool
	GenerateToken(ctx context.Context, clientID, clientSecret string) (*SatuSehatResponDTO, error)
}

// SatuSehatService struct for SATUSEHAT service
type SatuSehatServiceStruct struct {
	config     config.SatuSehatConfig
	httpClient *http.Client
	token      TokenDetail
	tokenMutex sync.RWMutex
}

// Token detail structure
type TokenDetail struct {
	AccessToken      string    `json:"access_token"`
	TokenType        string    `json:"token_type"`
	ExpiresIn        int64     `json:"expires_in"`
	IssuedAt         int64     `json:"issued_at"`
	OrganizationName string    `json:"organization_name"`
	DeveloperEmail   string    `json:"developer.email"`
	ClientID         string    `json:"client_id"`
	ApplicationName  string    `json:"application_name"`
	Status           string    `json:"status"`
	ExpiryTime       time.Time `json:"-"`
}

// Response structures
type SatuSehatResponMentahDTO struct {
	StatusCode int         `json:"status_code"`
	Success    bool        `json:"success"`
	Message    string      `json:"message"`
	Data       interface{} `json:"data"`
}

type SatuSehatResponDTO struct {
	StatusCode int         `json:"status_code"`
	Success    bool        `json:"success"`
	Message    string      `json:"message"`
	Data       interface{} `json:"data"`
	Error      *ErrorInfo  `json:"error,omitempty"`
}

type ErrorInfo struct {
	Code    string `json:"code"`
	Details string `json:"details"`
}

// Token methods
func (t *TokenDetail) IsExpired() bool {
	if t.ExpiryTime.IsZero() {
		return true
	}
	return time.Now().UTC().After(t.ExpiryTime.Add(-5 * time.Minute))
}

func (t *TokenDetail) SetExpired() {
	t.ExpiryTime = time.Time{}
}

// NewSatuSehatService creates a new SATUSEHAT service instance
func NewSatuSehatService(cfg config.SatuSehatConfig) SatuSehatService {
	service := &SatuSehatServiceStruct{
		config: cfg,
		httpClient: &http.Client{
			Timeout: cfg.Timeout,
		},
	}

	return service
}

// NewSatuSehatServiceFromConfig creates service from main config
func NewSatuSehatServiceFromConfig(cfg *config.Config) SatuSehatService {
	return NewSatuSehatService(cfg.SatuSehat)
}

// NewSatuSehatServiceFromInterface creates service from interface (for backward compatibility)
func NewSatuSehatServiceFromInterface(cfg interface{}) (SatuSehatService, error) {
	var satusehatConfig config.SatuSehatConfig

	// Try to map from interface
	err := smapping.FillStruct(&satusehatConfig, smapping.MapFields(&cfg))
	if err != nil {
		return nil, fmt.Errorf("failed to map config: %w", err)
	}

	if satusehatConfig.Timeout == 0 {
		satusehatConfig.Timeout = 30 * time.Second
	}

	return NewSatuSehatService(satusehatConfig), nil
}

// SetHTTPClient allows custom http client configuration
func (s *SatuSehatServiceStruct) SetHTTPClient(client *http.Client) {
	s.httpClient = client
}

// RefreshToken obtains new access token
func (s *SatuSehatServiceStruct) RefreshToken(ctx context.Context) error {
	s.tokenMutex.Lock()
	defer s.tokenMutex.Unlock()

	// Double-check pattern
	if !s.token.IsExpired() {
		return nil
	}

	// Remove duplicate /oauth2/v1 from URL since AuthURL already contains it
	tokenURL := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL)

	formData := fmt.Sprintf("client_id=%s&client_secret=%s", s.config.ClientID, s.config.ClientSecret)

	req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, bytes.NewBufferString(formData))
	if err != nil {
		return fmt.Errorf("failed to create token request: %w", err)
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	res, err := s.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to execute token request: %w", err)
	}
	defer res.Body.Close()

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

	if res.StatusCode != 200 {
		// Log the error response for debugging
		fmt.Printf("DEBUG: Token request failed with status %d: %s\n", res.StatusCode, string(body))
		return fmt.Errorf("token request failed with status %d: %s", res.StatusCode, string(body))
	}

	// Debug: log the raw response for troubleshooting
	fmt.Printf("DEBUG: SATUSEHAT token response - Status: %d, Body: %s\n", res.StatusCode, string(body))
	fmt.Printf("DEBUG: Request URL: %s\n", tokenURL)
	fmt.Printf("DEBUG: Request Headers: %+v\n", req.Header)

	return s.parseTokenResponse(body)
}

// parseTokenResponse parses token response from SATUSEHAT
func (s *SatuSehatServiceStruct) parseTokenResponse(body []byte) error {
	// Debug: log the raw response for detailed analysis
	fmt.Printf("DEBUG: Raw token response body: %s\n", string(body))

	result := gjson.ParseBytes(body)

	// Check if we have a valid access token
	accessToken := result.Get("access_token").String()
	if accessToken == "" {
		return fmt.Errorf("no access token found in response: %s", string(body))
	}

	issuedAt := result.Get("issued_at").Int()
	expiresIn := result.Get("expires_in").Int()

	// Handle timestamp conversion (issued_at could be in milliseconds or seconds)
	var expiryTime time.Time
	if issuedAt > 1000000000000 { // If timestamp is in milliseconds
		expiryTime = time.Unix(issuedAt/1000, 0).Add(time.Duration(expiresIn) * time.Second)
	} else if issuedAt > 0 { // If timestamp is in seconds
		expiryTime = time.Unix(issuedAt, 0).Add(time.Duration(expiresIn) * time.Second)
	} else {
		// If no issued_at, use current time + expires_in
		expiryTime = time.Now().UTC().Add(time.Duration(expiresIn) * time.Second)
	}

	s.token = TokenDetail{
		AccessToken:      accessToken,
		TokenType:        result.Get("token_type").String(),
		ExpiresIn:        expiresIn,
		IssuedAt:         issuedAt,
		OrganizationName: result.Get("organization_name").String(),
		DeveloperEmail:   result.Get("developer\\.email").String(),
		ClientID:         result.Get("client_id").String(),
		ApplicationName:  result.Get("application_name").String(),
		Status:           result.Get("status").String(),
		ExpiryTime:       expiryTime,
	}

	logger.Info("SATUSEHAT token refreshed successfully", map[string]interface{}{
		"expires_at":   s.token.ExpiryTime,
		"organization": s.token.OrganizationName,
		"token_type":   s.token.TokenType,
		"client_id":    s.token.ClientID,
	})

	return nil
}

// IsTokenValid checks if current token is valid
func (s *SatuSehatServiceStruct) IsTokenValid() bool {
	s.tokenMutex.RLock()
	defer s.tokenMutex.RUnlock()
	return !s.token.IsExpired()
}

// ensureValidToken ensures we have a valid token
func (s *SatuSehatServiceStruct) ensureValidToken(ctx context.Context) error {
	s.tokenMutex.RLock()
	needsRefresh := s.token.IsExpired()
	s.tokenMutex.RUnlock()

	if needsRefresh {
		return s.RefreshToken(ctx)
	}
	return nil
}

// prepareRequest prepares HTTP request with required headers
func (s *SatuSehatServiceStruct) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) {
	// Ensure valid token
	if err := s.ensureValidToken(ctx); err != nil {
		return nil, fmt.Errorf("failed to ensure valid token: %w", err)
	}

	fullURL := s.config.BaseURL + endpoint
	req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	// Set headers
	s.tokenMutex.RLock()
	token := s.token.AccessToken
	s.tokenMutex.RUnlock()

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))

	return req, nil
}

// processResponse processes response from SATUSEHAT API
func (s *SatuSehatServiceStruct) processResponse(res *http.Response) (*SatuSehatResponDTO, error) {
	defer res.Body.Close()

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

	// Create response
	resp := &SatuSehatResponDTO{
		StatusCode: res.StatusCode,
		Success:    res.StatusCode >= 200 && res.StatusCode < 300,
	}

	// Handle different status codes
	switch {
	case res.StatusCode == 401:
		s.tokenMutex.Lock()
		s.token.SetExpired()
		s.tokenMutex.Unlock()

		resp.Error = &ErrorInfo{
			Code:    "UNAUTHORIZED",
			Details: "Token expired or invalid",
		}
		resp.Message = "Unauthorized access"

	case res.StatusCode >= 400 && res.StatusCode < 500:
		resp.Error = &ErrorInfo{
			Code:    "CLIENT_ERROR",
			Details: string(body),
		}
		resp.Message = "Client error"

	case res.StatusCode >= 500:
		resp.Error = &ErrorInfo{
			Code:    "SERVER_ERROR",
			Details: string(body),
		}
		resp.Message = "Server error"

	default:
		resp.Message = "Success"
	}

	// Parse JSON response if successful
	if resp.Success && len(body) > 0 {
		var jsonData interface{}
		if err := json.Unmarshal(body, &jsonData); err != nil {
			// If JSON unmarshal fails, store as string
			resp.Data = string(body)
		} else {
			resp.Data = jsonData
		}
	}

	return resp, nil
}

// Get performs HTTP GET request
func (s *SatuSehatServiceStruct) Get(ctx context.Context, endpoint string, result interface{}) error {
	resp, err := s.GetRawResponse(ctx, endpoint)
	if err != nil {
		return err
	}

	return mapToResult(resp, result)
}

// Post performs HTTP POST request
func (s *SatuSehatServiceStruct) Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error {
	resp, err := s.PostRawResponse(ctx, endpoint, payload)
	if err != nil {
		return err
	}

	return mapToResult(resp, result)
}

// Put performs HTTP PUT request
func (s *SatuSehatServiceStruct) Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error {
	var buf bytes.Buffer
	if payload != nil {
		if err := json.NewEncoder(&buf).Encode(payload); err != nil {
			return fmt.Errorf("failed to encode payload: %w", err)
		}
	}

	req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf)
	if err != nil {
		return err
	}

	res, err := s.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to execute PUT request: %w", err)
	}

	resp, err := s.processResponse(res)
	if err != nil {
		return err
	}

	return mapToResult(resp, result)
}

// Delete performs HTTP DELETE request
func (s *SatuSehatServiceStruct) Delete(ctx context.Context, endpoint string, result interface{}) error {
	req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil)
	if err != nil {
		return err
	}

	res, err := s.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to execute DELETE request: %w", err)
	}

	resp, err := s.processResponse(res)
	if err != nil {
		return err
	}

	return mapToResult(resp, result)
}

// GetRawResponse returns raw response without mapping
func (s *SatuSehatServiceStruct) GetRawResponse(ctx context.Context, endpoint string) (*SatuSehatResponDTO, error) {
	req, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}

	res, err := s.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to execute GET request: %w", err)
	}

	return s.processResponse(res)
}

// PostRawResponse returns raw response without mapping
func (s *SatuSehatServiceStruct) PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*SatuSehatResponDTO, error) {
	var buf bytes.Buffer
	if payload != nil {
		if err := json.NewEncoder(&buf).Encode(payload); err != nil {
			return nil, fmt.Errorf("failed to encode payload: %w", err)
		}
	}

	req, err := s.prepareRequest(ctx, http.MethodPost, endpoint, &buf)
	if err != nil {
		return nil, err
	}

	res, err := s.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to execute POST request: %w", err)
	}

	return s.processResponse(res)
}

// FHIR-specific methods

// PostBundle posts FHIR bundle to SATUSEHAT
func (s *SatuSehatServiceStruct) PostBundle(ctx context.Context, bundle interface{}) (*SatuSehatResponDTO, error) {
	return s.PostRawResponse(ctx, "", bundle)
}

// GetPatientByNIK retrieves patient by NIK
func (s *SatuSehatServiceStruct) GetPatientByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error) {
	endpoint := fmt.Sprintf("/Patient?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik)
	return s.GetRawResponse(ctx, endpoint)
}

// GetPractitionerByNIK retrieves practitioner by NIK
func (s *SatuSehatServiceStruct) GetPractitionerByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error) {
	endpoint := fmt.Sprintf("/Practitioner?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik)
	return s.GetRawResponse(ctx, endpoint)
}

// GetResourceByID retrieves any FHIR resource by ID
func (s *SatuSehatServiceStruct) GetResourceByID(ctx context.Context, resourceType, id string) (*SatuSehatResponDTO, error) {
	endpoint := fmt.Sprintf("/%s/%s", resourceType, id)
	return s.GetRawResponse(ctx, endpoint)
}

// GenerateToken generates a new access token with custom client credentials
func (s *SatuSehatServiceStruct) GenerateToken(ctx context.Context, clientID, clientSecret string) (*SatuSehatResponDTO, error) {
	// Remove duplicate /oauth2/v1 from URL since AuthURL already contains it
	tokenURL := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL)

	formData := fmt.Sprintf("client_id=%s&client_secret=%s", clientID, clientSecret)

	req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, strings.NewReader(formData))
	if err != nil {
		return nil, fmt.Errorf("failed to create token request: %w", err)
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	res, err := s.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to execute token request: %w", err)
	}
	defer res.Body.Close()

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

	// Process the response using the existing response processor
	resp := &SatuSehatResponDTO{
		StatusCode: res.StatusCode,
		Success:    res.StatusCode >= 200 && res.StatusCode < 300,
	}

	// Handle different status codes
	switch {
	case res.StatusCode == 401:
		resp.Error = &ErrorInfo{
			Code:    "UNAUTHORIZED",
			Details: "Invalid client credentials",
		}
		resp.Message = "Unauthorized access"

	case res.StatusCode >= 400 && res.StatusCode < 500:
		resp.Error = &ErrorInfo{
			Code:    "CLIENT_ERROR",
			Details: string(body),
		}
		resp.Message = "Client error"

	case res.StatusCode >= 500:
		resp.Error = &ErrorInfo{
			Code:    "SERVER_ERROR",
			Details: string(body),
		}
		resp.Message = "Server error"

	default:
		resp.Message = "Success"
	}

	// Parse JSON response if successful
	if resp.Success && len(body) > 0 {
		var jsonData interface{}
		if err := json.Unmarshal(body, &jsonData); err != nil {
			// If JSON unmarshal fails, store as string
			resp.Data = string(body)
		} else {
			resp.Data = jsonData
		}
	}

	return resp, nil
}

// Helper functions

// mapToResult maps the final response to the result interface
func mapToResult(resp *SatuSehatResponDTO, result interface{}) error {
	respBytes, err := json.Marshal(resp)
	if err != nil {
		return fmt.Errorf("failed to marshal final response: %w", err)
	}

	if err := json.Unmarshal(respBytes, result); err != nil {
		return fmt.Errorf("failed to unmarshal to result: %w", err)
	}

	return nil
}

// Backward compatibility functions

func SatuSehatGetRequest(endpoint string, cfg interface{}) interface{} {
	service, err := NewSatuSehatServiceFromInterface(cfg)
	if err != nil {
		logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
			"error": err.Error(),
		})
		return nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	resp, err := service.GetRawResponse(ctx, endpoint)
	if err != nil {
		logger.Error("Failed to get SATUSEHAT response", map[string]interface{}{
			"error": err.Error(),
		})
		return nil
	}

	return resp
}

func SatuSehatPostRequest(endpoint string, cfg interface{}, data interface{}) interface{} {
	service, err := NewSatuSehatServiceFromInterface(cfg)
	if err != nil {
		logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
			"error": err.Error(),
		})
		return nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	resp, err := service.PostRawResponse(ctx, endpoint, data)
	if err != nil {
		logger.Error("Failed to post SATUSEHAT response", map[string]interface{}{
			"error": err.Error(),
		})
		return nil
	}

	return resp
}

// FHIR helper functions

func SatuSehatGetPatient(nik string, cfg interface{}) interface{} {
	service, err := NewSatuSehatServiceFromInterface(cfg)
	if err != nil {
		logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
			"error": err.Error(),
		})
		return nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	resp, err := service.GetPatientByNIK(ctx, nik)
	if err != nil {
		logger.Error("Failed to get patient", map[string]interface{}{
			"error": err.Error(),
			"nik":   nik,
		})
		return nil
	}

	return resp
}

func SatuSehatGetPractitioner(nik string, cfg interface{}) interface{} {
	service, err := NewSatuSehatServiceFromInterface(cfg)
	if err != nil {
		logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
			"error": err.Error(),
		})
		return nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	resp, err := service.GetPractitionerByNIK(ctx, nik)
	if err != nil {
		logger.Error("Failed to get practitioner", map[string]interface{}{
			"error": err.Error(),
			"nik":   nik,
		})
		return nil
	}

	return resp
}

func SatuSehatPostBundle(bundle interface{}, cfg interface{}) interface{} {
	service, err := NewSatuSehatServiceFromInterface(cfg)
	if err != nil {
		logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
			"error": err.Error(),
		})
		return nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	resp, err := service.PostBundle(ctx, bundle)
	if err != nil {
		logger.Error("Failed to post bundle", map[string]interface{}{
			"error": err.Error(),
		})
		return nil
	}

	return resp
}
