package medication

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"service/internal/infrastructure/database"
	extapi "service/internal/worker/interface"
	"service/pkg/logger"
)

const trackerFile = "internal/worker/satusehat/medication/last_medication_id.txt"
const mappingFile = "internal/worker/satusehat/medication/medication_mapping.txt"

type Config struct {
	DBManager       database.Service
	InternalBaseURL string
	OrganizationID  string
}

type TokenManager interface {
	GetAccessToken() string
	ForceRefreshAndGetToken(ctx context.Context) (string, error)
}

type WorkerService interface {
	Run(ctx context.Context)
}

type worker struct {
	cfg          Config
	repo         Repository
	apiClient    extapi.Client
	tokenManager TokenManager
}

func NewWorker(cfg Config, tokenManager TokenManager) WorkerService {
	return &worker{
		cfg:          cfg,
		repo:         NewRepository(cfg.DBManager),
		apiClient:    extapi.NewClient(60 * time.Second),
		tokenManager: tokenManager,
	}
}

func (w *worker) Run(ctx context.Context) {
	logger.Default().Info("[MEDICATION WORKER] Memulai proses sinkronisasi API...")

	w.ensureTrackerFileExists()
	lastID := w.readLastID()
	// if lastID == 0 {
	// 	lastID = 3384064 // Dikurangi 1 agar ID 3384065 ikut terambil
	// 	w.updateTracker(&lastID, lastID)
	// }
	logger.Default().Info("[MEDICATION WORKER] Melanjutkan proses dari", logger.Int64("last_id", lastID))

	for {
		select {
		case <-ctx.Done():
			logger.Default().Warn("[MEDICATION WORKER] Proses dihentikan oleh sistem (Graceful Shutdown)")
			return
		default:
		}

		med, err := w.repo.GetNextMedication(ctx, lastID)
		if err != nil {
			if err == sql.ErrNoRows {
				time.Sleep(5 * time.Second) // Tunggu data baru
				continue
			}
			logger.Default().Error("[MEDICATION WORKER] Gagal query ke database", logger.ErrorField(err))
			time.Sleep(5 * time.Second)
			continue
		}

		logger.Default().Info("[MEDICATION WORKER] 🔄 Memproses Dokumen",
			logger.Int64("idxpesanobat", med.IdxPesanObat),
			logger.String("kode_kfa", med.KfaCode.String),
		)

		if !med.KfaCode.Valid || med.KfaCode.String == "" {
			logger.Default().Warn("[MEDICATION WORKER] ⏭️ Skip Dokumen: KFA Code kosong",
				logger.Int64("idxpesanobat", med.IdxPesanObat),
			)
			w.updateTracker(&lastID, med.IdxPesanObat)
			continue
		}

		kfaDisplay, formCode, formDisplay := w.fetchKFADetails(ctx, med.KfaCode.String)

		internalPayload := MapToInternalAPI(med, w.cfg.OrganizationID, kfaDisplay, formCode, formDisplay)

		fhirID, respBody, status, errMsg, shouldRetry := w.sendToInternalAPI(ctx, internalPayload, med.IdxPesanObat)

		// Menyimpan payload request dan response ke dalam log
		logger.Default().Info("[MEDICATION WORKER] Detail Transaksi",
			logger.Int64("idxpesanobat", med.IdxPesanObat),
			logger.Any("request_payload", internalPayload),
			logger.String("response_payload", string(respBody)),
			logger.String("status", status),
		)

		syncLog := MapToSyncLog(med.IdxPesanObat, fhirID, internalPayload, respBody, status, errMsg)
		if errLog := w.repo.SaveSyncLog(ctx, syncLog); errLog != nil {
			logger.Default().Error("[MEDICATION WORKER] Gagal menyimpan histori log ke DB", logger.ErrorField(errLog))
		}

		if status == "SUCCESS" && fhirID != "" {
			// if errUpdate := w.repo.SaveSatuSehatID(ctx, med.IdxPesanObat, fhirID); errUpdate != nil {
			// 	logger.Default().Error("[MEDICATION WORKER] Gagal update FHIR ID ke transaksi utama", logger.ErrorField(errUpdate))
			// }
			w.saveMapping(med.IdxPesanObat, fhirID)
		}

		// SELALU update tracker agar lastID berjalan maju terus dan tidak mengulang data yang sama
		w.updateTracker(&lastID, med.IdxPesanObat)

		if shouldRetry {
			logger.Default().Warn("[MEDICATION WORKER] ⚠️ Gagal memproses data (melewati dokumen)", logger.Int64("idxpesanobat", med.IdxPesanObat), logger.String("reason", errMsg))
			time.Sleep(5 * time.Second)
		} else {
			time.Sleep(2 * time.Second)
		}
	}
}

func (w *worker) sendToInternalAPI(ctx context.Context, payload map[string]interface{}, idxPesanObat int64) (fhirID string, respBody []byte, status string, errMsg string, shouldRetry bool) {
	reqURL := fmt.Sprintf("%s/satusehat/medication", strings.TrimRight(w.cfg.InternalBaseURL, "/"))

	respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.tokenManager.GetAccessToken(), payload)
	if err != nil {
		logger.Default().Error("[MEDICATION WORKER] ❌ Gagal Koneksi ke API Part 3",
			logger.Int64("idxpesanobat", idxPesanObat),
			logger.Any("payload_sent", payload),
			logger.ErrorField(err),
		)
		return "", nil, "FAILED", err.Error(), true
	}

	if statusCode == http.StatusUnauthorized {
		logger.Default().Warn("[MEDICATION WORKER] Menerima status 401 Unauthorized, mencoba refresh token...", logger.Int64("idxpesanobat", idxPesanObat))

		newToken, refreshErr := w.tokenManager.ForceRefreshAndGetToken(ctx)
		if refreshErr != nil {
			logger.Default().Error("[MEDICATION WORKER] Gagal refresh token, akan mencoba lagi pada iterasi berikutnya.", logger.Int64("idxpesanobat", idxPesanObat), logger.ErrorField(refreshErr))
			return "", respBody, "FAILED", refreshErr.Error(), true
		}

		logger.Default().Info("[MEDICATION WORKER] Token berhasil di-refresh, mencoba ulang request...", logger.Int64("idxpesanobat", idxPesanObat))
		respBody, statusCode, err = w.apiClient.PostJSON(ctx, reqURL, newToken, payload)
		if err != nil {
			logger.Default().Error("[MEDICATION WORKER] ❌ Gagal Koneksi ke API Part 3 (setelah retry)",
				logger.Int64("idxpesanobat", idxPesanObat),
				logger.ErrorField(err),
			)
			return "", nil, "FAILED", err.Error(), true
		}
	}

	if statusCode != http.StatusOK && statusCode != http.StatusCreated {
		errMsg = fmt.Sprintf("HTTP %d", statusCode)
		logger.Default().Error("[MEDICATION WORKER] ❌ Respons Error dari API Part 3",
			logger.Int64("idxpesanobat", idxPesanObat),
			logger.Int("status_code", statusCode),
			logger.String("response", truncate(string(respBody), 200)),
		)

		if statusCode >= 500 || statusCode == http.StatusTooManyRequests {
			return "", respBody, "FAILED", errMsg, true
		}
		return "", respBody, "FAILED", errMsg, false
	}

	var result map[string]interface{}
	if errUnmarshal := json.Unmarshal(respBody, &result); errUnmarshal == nil {
		if data, ok := result["data"].(map[string]interface{}); ok {
			if id, ok := data["id"].(string); ok {
				fhirID = id
			}
		} else if id, ok := result["id"].(string); ok {
			fhirID = id
		}
	}
	logger.Default().Info("[MEDICATION WORKER] ✅ Sukses mengirim data Medication",
		logger.Int64("idxpesanobat", idxPesanObat),
		logger.String("fhir_id", fhirID),
	)

	return fhirID, respBody, "SUCCESS", "", false
}

func (w *worker) fetchKFADetails(ctx context.Context, kfaCode string) (kfaDisplay, formCode, formDisplay string) {
	reqURL := fmt.Sprintf("%s/satusehat/reference/kfa/products/%s", strings.TrimRight(w.cfg.InternalBaseURL, "/"), kfaCode)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
	if err != nil {
		logger.Default().Error("[MEDICATION WORKER] Gagal membuat request KFA", logger.ErrorField(err))
		return "", "", ""
	}

	req.Header.Set("Authorization", "Bearer "+w.tokenManager.GetAccessToken())
	req.Header.Set("Accept", "application/json")

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		logger.Default().Error("[MEDICATION WORKER] Gagal request data ke Part 3 KFA", logger.ErrorField(err))
		return "", "", ""
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		logger.Default().Warn("[MEDICATION WORKER] KFA tidak ditemukan / Error", logger.Int("status", resp.StatusCode), logger.String("kfa_code", kfaCode))
		return "", "", ""
	}

	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		logger.Default().Error("[MEDICATION WORKER] Gagal decode response KFA", logger.ErrorField(err))
		return "", "", ""
	}

	// Parsing respons bersarang dari KFA API (data -> result -> dosage_form)
	if dataMap, ok := result["data"].(map[string]interface{}); ok {
		if resultMap, ok := dataMap["result"].(map[string]interface{}); ok {
			// Ekstrak nama obat
			if val, ok := resultMap["name"].(string); ok {
				kfaDisplay = val
			}

			// Ekstrak bentuk obat (dosage_form)
			if dosageForm, ok := resultMap["dosage_form"].(map[string]interface{}); ok {
				if val, ok := dosageForm["code"].(string); ok {
					formCode = val
				}
				if val, ok := dosageForm["name"].(string); ok {
					formDisplay = val
				}
			}
		}
	}

	// Fallback jika format respons langsung flat
	if kfaDisplay == "" {
		if val, ok := result["name"].(string); ok {
			kfaDisplay = val
		}
	}
	if formCode == "" {
		if val, ok := result["form_code"].(string); ok {
			formCode = val
		}
	}
	if formDisplay == "" {
		if val, ok := result["form_display"].(string); ok {
			formDisplay = val
		}
	}

	return kfaDisplay, formCode, formDisplay
}

func (w *worker) updateTracker(lastID *int64, currentID int64) {
	*lastID = currentID
	os.MkdirAll("internal/worker/satusehat/medication", 0755)
	os.WriteFile(trackerFile, []byte(strconv.FormatInt(currentID, 10)), 0644)
}

func (w *worker) readLastID() int64 {
	data, err := os.ReadFile(trackerFile)
	if err != nil {
		return 0
	}
	idStr := strings.TrimSpace(string(data))
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		logger.Default().Warn("[MEDICATION WORKER] Gagal parsing ID dari tracker file, memulai dari awal.", logger.String("content", idStr), logger.ErrorField(err))
		return 0
	}
	return id
}

func (w *worker) ensureTrackerFileExists() {
	if _, err := os.Stat(trackerFile); os.IsNotExist(err) {
		dir := filepath.Dir(trackerFile)
		if err := os.MkdirAll(dir, 0755); err != nil {
			logger.Default().Error("[MEDICATION WORKER] Gagal membuat direktori tracker", logger.ErrorField(err))
		}
		if err := os.WriteFile(trackerFile, []byte("0"), 0644); err != nil {
			logger.Default().Error("[MEDICATION WORKER] Gagal membuat file tracker default", logger.ErrorField(err))
		}
	}
}

func (w *worker) saveMapping(idxPesanObat int64, fhirID string) {
	os.MkdirAll("internal/worker/satusehat/medication", 0755)
	f, err := os.OpenFile(mappingFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		logger.Default().Error("[MEDICATION WORKER] Gagal membuka file mapping", logger.ErrorField(err))
		return
	}
	defer f.Close()

	line := fmt.Sprintf("%d|%s\n", idxPesanObat, fhirID)
	if _, err := f.WriteString(line); err != nil {
		logger.Default().Error("[MEDICATION WORKER] Gagal menulis ke file mapping", logger.ErrorField(err))
	}
}

func truncate(s string, max int) string {
	if len(s) > max {
		return s[:max] + "..."
	}
	return s
}
