package auth

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strings"

	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	"github.com/TaruLab/keluargamart-backend/pkg/logger"
	"github.com/TaruLab/keluargamart-backend/pkg/middleware"
)

// LoginRequest mirrors LoginRequest in openapi.yaml.
type LoginRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
	App      string `json:"app,omitempty"`
}

// UpdateProfileRequest mirrors UpdateProfileRequest in openapi.yaml.
type UpdateProfileRequest struct {
	Name            string `json:"name"`
	Email           string `json:"email"`
	CurrentPassword string `json:"current_password"`
	Password        string `json:"password"`
}

// Handler binds auth service to HTTP.
type Handler struct {
	svc *Service
	// customerFlags, when wired (main.go), enriches GET /auth/me with
	// commerce-identity attributes of the linked customer (RBAC audit
	// 2026-08-25: e-commerce application scope). Nil-safe.
	customerFlags func(ctx context.Context, userID int64) map[string]any
	// storeResolver returns the store ids the user belongs to (store_users).
	// Nil-safe; when set, GET /auth/me includes them so frontends (POS) can
	// scope stock adjustments to the logged-in toko's warehouse.
	storeResolver func(ctx context.Context, userID int64) []int64
	// storeDetailsResolver returns the same memberships as whole records —
	// id, code, name, address, phone and the bound warehouse_id. Nil-safe;
	// when set, GET /auth/me carries `store_details` so the POS can name the
	// active outlet and scope stock to its warehouse without a second call.
	storeDetailsResolver func(ctx context.Context, userID int64) []StoreRef
}

// StoreRef is the per-outlet identity the POS needs from GET /auth/me:
// enough to label the active outlet and resolve its stock warehouse.
type StoreRef struct {
	ID          int64  `json:"id"`
	Code        string `json:"code"`
	Name        string `json:"name"`
	Address     string `json:"address,omitempty"`
	Phone       string `json:"phone,omitempty"`
	IsActive    bool   `json:"is_active"`
	WarehouseID int64  `json:"warehouse_id"`
}

func NewHandler(svc *Service) *Handler {
	return &Handler{svc: svc}
}

// WithCustomerFlagsResolver wires the commerce-identity resolver.
func (h *Handler) WithCustomerFlagsResolver(f func(context.Context, int64) map[string]any) *Handler {
	h.customerFlags = f
	return h
}

// WithStoreResolver wires the store_users lookup for GET /auth/me.
func (h *Handler) WithStoreResolver(f func(context.Context, int64) []int64) *Handler {
	h.storeResolver = f
	return h
}

// WithStoreDetailsResolver wires the richer store_users lookup (whole records
// incl. warehouse_id) for GET /auth/me.
func (h *Handler) WithStoreDetailsResolver(f func(context.Context, int64) []StoreRef) *Handler {
	h.storeDetailsResolver = f
	return h
}

// Login handles POST /api/v1/auth/login.
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
	var req LoginRequest
	if err := decodeJSON(w, r, &req); err != nil {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, httpresponse.SafeMessage(err), nil)
		return
	}
	if req.Email == "" || req.Password == "" {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, "Email and password are required", nil)
		return
	}
	appScope := strings.TrimSpace(strings.ToLower(req.App))
	if appScope == "" {
		appScope = strings.TrimSpace(strings.ToLower(r.Header.Get("X-Application")))
	}
	if appScope == "" {
		appScope = strings.TrimSpace(strings.ToLower(r.Header.Get("X-App")))
	}
	if appScope == "" {
		origin := strings.ToLower(r.Header.Get("Origin") + " " + r.Header.Get("Referer"))
		if strings.Contains(origin, "erp") || strings.Contains(origin, ":3001") {
			appScope = "erp"
		} else if strings.Contains(origin, "pos") || strings.Contains(origin, ":3003") {
			appScope = "pos"
		}
	}
	result, err := h.svc.Login(r.Context(), req.Email, req.Password, appScope)
	if err != nil {
		switch err {
		case ErrInvalidCredentials:
			httpresponse.Error(w, http.StatusUnauthorized, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeAuthentication, "Email tidak terdaftar atau password salah", nil)
		case ErrVerificationPending:
			httpresponse.Error(w, http.StatusForbidden, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeEmailNotVerified, "Email belum diverifikasi", map[string][]string{
					"email": []string{req.Email},
				})
		case ErrAccountDisabled:
			httpresponse.Error(w, http.StatusForbidden, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeAccountDisabled, "Akun Anda telah dinonaktifkan", nil)
		default:
			httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		}
		return
	}
	if appScope != "" {
		targetApp := ApplicationAccess(appScope)
		if targetApp == AppECommerce {
			isBuyer := result.User.Role == RoleBuyer || result.User.Role == "customer" || result.User.Role == "b2b_client"
			isEcomScope := result.User.CanAccessApp(AppECommerce) || result.User.CanAccessApp(AppECommerceAdmin)
			if !isBuyer || !isEcomScope {
				httpresponse.Error(w, http.StatusForbidden, middleware.RequestIDFrom(r.Context()),
					httpresponse.CodeForbidden, "Akun Anda tidak memiliki izin untuk mengakses B2B Storefront. Hanya akun pelanggan (Buyer) yang diizinkan.", nil)
				return
			}
		} else if targetApp == AppERP {
			if !result.User.CanAccessApp(AppERP) {
				httpresponse.Error(w, http.StatusForbidden, middleware.RequestIDFrom(r.Context()),
					httpresponse.CodeForbidden, "Akun ini tidak memiliki hak akses ke aplikasi ERP. Hubungi administrator.", nil)
				return
			}
		} else if targetApp == AppPOS {
			if !result.User.CanAccessApp(AppPOS) {
				httpresponse.Error(w, http.StatusForbidden, middleware.RequestIDFrom(r.Context()),
					httpresponse.CodeForbidden, "Akun ini tidak memiliki hak akses ke aplikasi POS. Hubungi administrator.", nil)
				return
			}
		} else if !result.User.CanAccessApp(targetApp) {
			httpresponse.Error(w, http.StatusForbidden, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeForbidden, "Akun Anda tidak memiliki akses ke aplikasi ini", nil)
			return
		}
	}
	payload := map[string]any{
		"token":       result.Token,
		"user":        result.User,
		"permissions": result.User.EffectivePermissions(),
	}
	if h.storeResolver != nil {
		if ids := h.storeResolver(r.Context(), result.User.ID); ids != nil {
			payload["stores"] = ids
		}
	}
	if h.storeDetailsResolver != nil {
		if refs := h.storeDetailsResolver(r.Context(), result.User.ID); refs != nil {
			payload["store_details"] = refs
			if len(refs) > 0 && refs[0].WarehouseID > 0 && result.User.WarehouseID == nil {
				wid := refs[0].WarehouseID
				result.User.WarehouseID = &wid
			}
		}
	}
	if h.customerFlags != nil {
		if flags := h.customerFlags(r.Context(), result.User.ID); flags != nil {
			payload["customer"] = flags
			if admin, ok := flags["is_ecommerce_admin"].(bool); ok && admin {
				result.User.ApplicationAccess = AppECommerceAdmin
			}
		}
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), payload)
}

// RegisterRequest mirrors RegisterRequest in openapi.yaml.
type RegisterRequest struct {
	AccountType string `json:"account_type"`
	Name        string `json:"name"`
	Email       string `json:"email"`
	Password    string `json:"password"`
	Phone       string `json:"phone"`
	CompanyName string `json:"company_name"`
	TaxNumber   string `json:"tax_number"`
	// ReferralCode is business-only (sales employee_number); the service
	// rejects it for individuals and unknown codes.
	ReferralCode string `json:"referral_code"`
}

// Register handles POST /api/v1/auth/register (public self-service signup).
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
	var req RegisterRequest
	if err := decodeJSON(w, r, &req); err != nil {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, httpresponse.SafeMessage(err), nil)
		return
	}
	result, err := h.svc.Register(r.Context(), RegisterInput{
		AccountType:  req.AccountType,
		Name:         req.Name,
		Email:        req.Email,
		Password:     req.Password,
		Phone:        req.Phone,
		CompanyName:  req.CompanyName,
		TaxNumber:    req.TaxNumber,
		ReferralCode: req.ReferralCode,
	})
	if err != nil {
		switch err {
		case ErrRegisterInvalid:
			httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeValidation, "Data pendaftaran tidak lengkap atau tidak valid", nil)
		case ErrInvalidReferral:
			httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeValidation, "Kode referral sales tidak valid", nil)
		case ErrEmailTaken:
			httpresponse.Error(w, http.StatusConflict, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeConflict, "Email sudah terdaftar. Silakan masuk.", nil)
		default:
			httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		}
		return
	}
	// 000152: no token in the register response anymore — the session is
	// issued by POST /auth/verify-email once the emailed 6-digit code is
	// confirmed. The B2B client routes to its OTP screen from here.
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), map[string]any{
		"email":                 result.Email,
		"verification_required": result.VerificationRequired,
		"message":               "Kode verifikasi telah dikirim ke email Anda. Masukkan kode untuk mengaktifkan akun.",
	})
}

// VerifyEmailRequest is the body of POST /auth/verify-email (000152).
type VerifyEmailRequest struct {
	Email string `json:"email"`
	Code  string `json:"code"`
}

// VerifyEmail handles POST /auth/verify-email: swaps the emailed 6-digit code
// for the session Register no longer issues. Same rate limiter as login —
// the endpoint validates a secret over an unauthenticated channel.
func (h *Handler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
	var req VerifyEmailRequest
	if err := decodeJSON(w, r, &req); err != nil {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, httpresponse.SafeMessage(err), nil)
		return
	}
	result, err := h.svc.VerifyEmail(r.Context(), req.Email, req.Code)
	if err != nil {
		switch err {
		case ErrRegisterInvalid:
			httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeValidation, "Email tidak valid", nil)
		case ErrCodeInvalid:
			httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeValidation, "Kode verifikasi salah atau kedaluwarsa", nil)
		case ErrCodeLocked:
			httpresponse.Error(w, http.StatusTooManyRequests, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeRateLimited, "Terlalu banyak percobaan. Minta kode baru.", nil)
		case ErrAlreadyVerified:
			httpresponse.Error(w, http.StatusConflict, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeConflict, "Email sudah terverifikasi. Silakan masuk.", nil)
		default:
			httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		}
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), map[string]any{
		"token":       result.Token,
		"user":        result.User,
		"permissions": result.User.EffectivePermissions(),
	})
}

// ResendVerification handles POST /auth/resend-verification: re-issues the
// 6-digit code. Always 200 with a generic message — the endpoint cannot be
// used to discover whether an email is registered or already verified.
func (h *Handler) ResendVerification(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Email string `json:"email"`
	}
	if err := decodeJSON(w, r, &req); err != nil {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, httpresponse.SafeMessage(err), nil)
		return
	}
	if err := h.svc.ResendVerification(r.Context(), req.Email); err != nil && err != ErrRegisterInvalid {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), map[string]any{
		"message": "Jika email terdaftar dan belum terverifikasi, kode verifikasi baru telah dikirim.",
	})
}

// Logout handles POST /api/v1/auth/logout (revokes current token).
func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.Logout(r.Context(), TokenIDFrom(r.Context())); err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// Me handles GET /api/v1/auth/me.
func (h *Handler) Me(w http.ResponseWriter, r *http.Request) {
	user := UserFrom(r.Context())
	fresh, err := h.svc.Me(r.Context(), user)
	if err != nil {
		if err == ErrNotFoundUser {
			httpresponse.Error(w, http.StatusNotFound, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeNotFound, "User not found", nil)
			return
		}
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	payload := map[string]any{
		"user":        fresh,
		"employee":    nil, // employee lookup lands with HR module (Phase 8)
		"permissions": fresh.EffectivePermissions(),
	}
	if h.storeResolver != nil {
		if ids := h.storeResolver(r.Context(), fresh.ID); ids != nil {
			payload["stores"] = ids
		}
	}
	if h.storeDetailsResolver != nil {
		if refs := h.storeDetailsResolver(r.Context(), fresh.ID); refs != nil {
			payload["store_details"] = refs
			if len(refs) > 0 && refs[0].WarehouseID > 0 && fresh.WarehouseID == nil {
				wid := refs[0].WarehouseID
				fresh.WarehouseID = &wid
			}
		}
	}
	if h.customerFlags != nil {
		if flags := h.customerFlags(r.Context(), fresh.ID); flags != nil {
			payload["customer"] = flags
			// Promote to ecommerce_admin scope when the linked customer holds
			// the backoffice flag (never inferred from the platform role).
			if admin, ok := flags["is_ecommerce_admin"].(bool); ok && admin {
				fresh.ApplicationAccess = AppECommerceAdmin
			}
		}
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), payload)
}

// UpdateMe handles PUT /api/v1/auth/me.
func (h *Handler) UpdateMe(w http.ResponseWriter, r *http.Request) {
	var req UpdateProfileRequest
	if err := decodeJSON(w, r, &req); err != nil {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, httpresponse.SafeMessage(err), nil)
		return
	}
	u, err := h.svc.UpdateProfile(r.Context(), UserFrom(r.Context()).ID, req.Name, req.Email, req.CurrentPassword, req.Password)
	if err != nil {
		// Surface only user-actionable validation errors; anything else is an
		// internal failure logged server-side (never leak the raw error).
		switch {
		case errors.Is(err, ErrInvalidCredentials),
			errors.Is(err, ErrAccountDisabled),
			errors.Is(err, ErrNotFoundUser):
			httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeValidation, err.Error(), nil)
		default:
			httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		}
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), u)
}

// ForgotPassword handles POST /api/v1/auth/forgot-password. The response is
// always 200 with a generic anti-enumeration message and NEVER includes the
// reset token or link (2026-09-10 finding: returning it in the response body
// is an account-takeover vector). Delivery is exclusively via email — see
// Service.RequestPasswordReset / WithMailer.
func (h *Handler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Email string `json:"email"`
	}
	if err := decodeJSON(w, r, &req); err != nil {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, httpresponse.SafeMessage(err), nil)
		return
	}
	if _, err := h.svc.RequestPasswordReset(r.Context(), req.Email); err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()),
		map[string]string{"message": "Jika email terdaftar, tautan reset telah dikirim."})
}

// ResetPassword handles POST /api/v1/auth/reset-password.
func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Token    string `json:"token"`
		Password string `json:"password"`
	}
	if err := decodeJSON(w, r, &req); err != nil {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, httpresponse.SafeMessage(err), nil)
		return
	}
	if err := h.svc.ResetPassword(r.Context(), req.Token, req.Password); err != nil {
		// The service returns its own validation errors with wording written for
		// the user, but it also passes the store's errors straight through, so
		// this goes through the boundary rather than assuming every error here
		// is safe to show.
		httpresponse.SafeError(w, middleware.RequestIDFrom(r.Context()),
			logger.FromContext(r.Context()), http.StatusBadRequest, httpresponse.CodeValidation, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), map[string]string{"message": "Kata sandi berhasil diubah. Silakan masuk dengan kata sandi baru."})
}

func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) error {
	defer r.Body.Close()
	return json.NewDecoder(r.Body).Decode(dst)
}
