package repo

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// PatientData holds the response from the patient-data API.
type PatientData struct {
	AccessionNumber  string `json:"accession_number"`
	MedrecID         string `json:"medrec_id"`
	RegID            string `json:"reg_id"`
	PatientName      string `json:"patient_name"`
	Modality         string `json:"modality"`
	StudyDescription string `json:"study_description"`
}

// PatientRepo is an HTTP client for the external patient-data API.
type PatientRepo struct {
	baseURL    string
	endpoint   string
	httpClient *http.Client
	authHeader string
	authToken  string
	retryCount int
	retrySleep time.Duration
}

// NewPatientRepo creates a new PatientRepo.
func NewPatientRepo(baseURL, endpoint, authHeader, authToken string, timeout time.Duration, retry int, retryBackoff time.Duration) *PatientRepo {
	return &PatientRepo{
		baseURL:  strings.TrimRight(baseURL, "/"),
		endpoint: endpoint,
		httpClient: &http.Client{
			Timeout: timeout,
		},
		authHeader: authHeader,
		authToken:  authToken,
		retryCount: retry,
		retrySleep: retryBackoff,
	}
}

// ByAccessionNumber fetches patient data for the given accession number.
// Returns (nil, nil) if:
//   - patient API is not configured (empty base URL)
//   - accession number is not found (HTTP 404)
// This allows callers to gracefully degrade when patient info is unavailable.
func (r *PatientRepo) ByAccessionNumber(ctx context.Context, acc string) (*PatientData, error) {
	if r.baseURL == "" {
		return nil, nil
	}

	u, err := url.Parse(r.baseURL + r.endpoint)
	if err != nil {
		return nil, fmt.Errorf("invalid patient API URL: %w", err)
	}
	u.RawQuery = url.Values{"accession_number": {acc}}.Encode()

	var lastErr error
	for attempt := 0; attempt <= r.retryCount; attempt++ {
		if attempt > 0 {
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			case <-time.After(r.retrySleep):
			}
		}

		data, err := r.fetchPatientData(ctx, u.String(), acc)
		if err == nil {
			return data, nil
		}
		lastErr = err

		if !isRetryable(err) {
			return nil, lastErr
		}
	}

	return nil, lastErr
}

// fetchPatientData performs a single HTTP request to the patient API.
func (r *PatientRepo) fetchPatientData(ctx context.Context, requestURL, acc string) (*PatientData, error) {
	req, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	if r.authHeader != "" && r.authToken != "" {
		req.Header.Set(r.authHeader, r.authToken)
	}

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

	switch resp.StatusCode {
	case http.StatusOK:
		var data PatientData
		if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
			return nil, fmt.Errorf("decode patient API response: %w", err)
		}
		return &data, nil

	case http.StatusNotFound:
		return nil, nil // not found is not an error — gracefully degrade

	default:
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("patient API returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
	}
}

// isRetryable returns true if the error is a transient network error
// that might succeed on retry.
func isRetryable(err error) bool {
	msg := err.Error()
	return strings.Contains(msg, "timeout") ||
		strings.Contains(msg, "connection refused") ||
		strings.Contains(msg, "no such host") ||
		strings.Contains(msg, "connection reset") ||
		strings.Contains(msg, "Temporary failure")
}
