﻿package settings

import (
	"context"
	"encoding/json"
	"net/http"
	"sync"

	"github.com/TaruLab/keluargamart-backend/internal/db"
	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	"github.com/TaruLab/keluargamart-backend/pkg/middleware"
	"github.com/jackc/pgx/v5/pgxpool"
)

type StoreConfig struct {
	Name       string `json:"name"`
	Tagline    string `json:"tagline"`
	ParentLine string `json:"parentLine"`
	Address    string `json:"address"`
	Phone      string `json:"phone"`
}

type GalonConfig struct {
	HargaIsi   int `json:"hargaIsi"`
	HargaGalon int `json:"hargaGalon"`
	Deposit    int `json:"deposit"`
	BiayaAntar int `json:"biayaAntar"`
}

type SystemSettings struct {
	Store StoreConfig `json:"store"`
	Galon GalonConfig `json:"galon"`
}

// appSettingsKey is the single app_settings row holding the SystemSettings
// JSON document (multi-tenant §7: persisted settings; one global doc for now
// — per-store docs land with store-scoped settings UI).
const appSettingsKey = "system"

type Handler struct {
	mu       sync.RWMutex
	settings SystemSettings
	q        *db.Queries
}

func NewHandler(pool *pgxpool.Pool) *Handler {
	h := &Handler{
		settings: SystemSettings{
			Store: StoreConfig{
				Name:       "KeluargaMart",
				Tagline:    "Minimarket & Distributor",
				ParentLine: "PT Sinar Retailindo Utama",
				Address:    "Jl. Merdeka No. 10, Medan",
				Phone:      "061-881234",
			},
			Galon: GalonConfig{
				HargaIsi:   6000,
				HargaGalon: 50000,
				Deposit:    35000,
				BiayaAntar: 5000,
			},
		},
	}
	if pool != nil {
		h.q = db.New(pool)
		if raw, err := h.q.GetAppSetting(context.Background(), appSettingsKey); err == nil {
			_ = json.Unmarshal(raw, &h.settings)
		}
	}
	return h
}

// save persists doc. It takes the document as an argument rather than reading
// h.settings, so a caller can only write a value it has decided on — the cached
// copy is never the thing being persisted, which is what let a failed write
// leave the cache holding something the database never accepted.
func (h *Handler) save(doc SystemSettings) error {
	raw, err := json.Marshal(doc)
	if err != nil {
		return err
	}
	return h.q.UpsertAppSetting(context.Background(), db.UpsertAppSettingParams{Key: appSettingsKey, Value: raw})
}

func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
	h.mu.RLock()
	defer h.mu.RUnlock()
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), h.settings)
}

// GalonSnapshot returns a copy of the galon price list for server-side use
// (galon flow prices authoritatively from here, never from client input).
func (h *Handler) GalonSnapshot() GalonConfig {
	h.mu.RLock()
	defer h.mu.RUnlock()
	return h.settings.Galon
}

func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
	var in struct {
		Store *StoreConfig `json:"store"`
		Galon *GalonConfig `json:"galon"`
	}
	defer r.Body.Close()
	if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, "invalid settings payload", nil)
		return
	}
	if in.Galon != nil {
		// Two separate problems, both about a price silently becoming 0.
		//
		// 1. A price of 0 means "free". Free refills and free galons are not a
		//    configuration, they are a mistake, and the old check only rejected
		//    negatives so 0 sailed through. Deposit and delivery fee are
		//    different: a store may genuinely charge no deposit, and free
		//    delivery is a normal promotion, so 0 stays legal for those two.
		if in.Galon.HargaIsi <= 0 {
			httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()), httpresponse.CodeValidation,
				"hargaIsi must be greater than 0 — a free refill is not a valid price list", nil)
			return
		}
		if in.Galon.HargaGalon <= 0 {
			httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()), httpresponse.CodeValidation,
				"hargaGalon must be greater than 0 — a free galon is not a valid price list", nil)
			return
		}
		if in.Galon.Deposit < 0 || in.Galon.BiayaAntar < 0 {
			httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()), httpresponse.CodeValidation,
				"deposit and biayaAntar must be >= 0 (0 = none is valid for both)", nil)
			return
		}
	}

	h.mu.Lock()
	defer h.mu.Unlock()

	// Build the candidate on a copy, persist it, and only then adopt it.
	//
	// The assignments used to land on h.settings directly and save() ran
	// afterwards, so a failed write left the in-memory copy holding a document
	// that existed nowhere else. The response was correctly 500 — and then the
	// process served the unsaved values anyway. GET /settings showed the
	// operator a save that never happened, and GalonSnapshot, wired in main.go
	// as galon.PriceList, billed galon transactions at prices from a
	// configuration that was not stored; a restart then priced the same
	// transaction differently. Persist-first makes the cache agree with the
	// database by construction instead of by careful ordering of side effects.
	next := h.settings
	if in.Store != nil {
		if in.Store.Name != "" {
			next.Store.Name = in.Store.Name
		}
		if in.Store.Tagline != "" {
			next.Store.Tagline = in.Store.Tagline
		}
		if in.Store.ParentLine != "" {
			next.Store.ParentLine = in.Store.ParentLine
		}
		if in.Store.Address != "" {
			next.Store.Address = in.Store.Address
		}
		if in.Store.Phone != "" {
			next.Store.Phone = in.Store.Phone
		}
	}
	if in.Galon != nil {
		next.Galon = *in.Galon
	}

	if h.q != nil {
		if err := h.save(next); err != nil {
			httpresponse.Error(w, http.StatusInternalServerError, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeInternal, "persist settings", nil)
			return
		}
	}
	h.settings = next

	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), h.settings)
}
