package accounting

import (
	"encoding/csv"
	"errors"
	"fmt"
	"net/http"
	"strconv"

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	"github.com/TaruLab/keluargamart-backend/pkg/logger"
	"github.com/TaruLab/keluargamart-backend/pkg/middleware"
	"github.com/go-chi/chi/v5"
)

// Handler binds the accounting service to HTTP. Methods are exported so the
// router can attach RequirePermission per route in main.
type Handler struct {
	svc *Service
}

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

// --- helpers

func pathID(r *http.Request) int64 {
	id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
	return id
}

func queryInt(s string) int {
	n, _ := strconv.Atoi(s)
	return n
}

func queryInt64(s string) int64 {
	n, _ := strconv.ParseInt(s, 10, 64)
	return n
}

func queryBoolPtr(s string) *bool {
	if s == "" {
		return nil
	}
	b := s == "true"
	return &b
}

func callerID(r *http.Request) *int64 {
	if u := auth.UserFrom(r.Context()); u != nil {
		return &u.ID
	}
	return nil
}

func respondErr(w http.ResponseWriter, r *http.Request, err error) {
	rid := middleware.RequestIDFrom(r.Context())
	switch {
	case errors.Is(err, ErrNotFound):
		httpresponse.Error(w, http.StatusNotFound, rid, httpresponse.CodeNotFound, "Not found", nil)
	case errors.Is(err, ErrValidation), errors.Is(err, ErrInvalidDate), errors.Is(err, ErrBadReference):
		httpresponse.Error(w, http.StatusBadRequest, rid, httpresponse.CodeValidation, err.Error(), nil)
	case errors.Is(err, ErrDuplicate), errors.Is(err, ErrNotDraft), errors.Is(err, ErrUnbalanced),
		errors.Is(err, ErrPeriodLocked), errors.Is(err, ErrJournalImmutable), errors.Is(err, ErrJournalIntegrity), errors.Is(err, ErrJournalVoidForbidden):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	default:
		httpresponse.Internal(w, rid, logger.FromContext(r.Context()), err)
	}
}

func decode(w http.ResponseWriter, r *http.Request, dst any) bool {
	return httpresponse.DecodeJSON(w, r, middleware.RequestIDFrom(r.Context()), dst)
}

// --- accounts -----------------------------------------------------------------

func (h *Handler) ListAccounts(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	accounts, err := h.svc.ListAccounts(r.Context(), queryBoolPtr(q.Get("active")), q.Get("search"))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), accounts)
}

func (h *Handler) CreateAccount(w http.ResponseWriter, r *http.Request) {
	var in AccountInput
	if !decode(w, r, &in) {
		return
	}
	a, err := h.svc.CreateAccount(r.Context(), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), a)
}

func (h *Handler) UpdateAccount(w http.ResponseWriter, r *http.Request) {
	var in AccountInput
	if !decode(w, r, &in) {
		return
	}
	a, err := h.svc.UpdateAccount(r.Context(), pathID(r), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), a)
}

// --- departments ----------------------------------------------------------------

func (h *Handler) ListDepartments(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	deps, err := h.svc.ListDepartments(r.Context(), queryBoolPtr(q.Get("active")), q.Get("search"))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), deps)
}

func (h *Handler) CreateDepartment(w http.ResponseWriter, r *http.Request) {
	var in DepartmentInput
	if !decode(w, r, &in) {
		return
	}
	d, err := h.svc.CreateDepartment(r.Context(), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), d)
}

func (h *Handler) UpdateDepartment(w http.ResponseWriter, r *http.Request) {
	var in DepartmentInput
	if !decode(w, r, &in) {
		return
	}
	d, err := h.svc.UpdateDepartment(r.Context(), pathID(r), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), d)
}

// --- journals -----------------------------------------------------------------

func journalFilterFromQuery(r *http.Request) JournalFilter {
	q := r.URL.Query()
	return JournalFilter{
		Page:        queryInt(q.Get("page")),
		PerPage:     queryInt(q.Get("per_page")),
		Search:      q.Get("search"),
		Status:      q.Get("status"),
		JournalType: q.Get("journal_type"),
		DateFrom:    q.Get("date_from"),
		DateTo:      q.Get("date_to"),
		StoreID:     queryInt64(q.Get("store_id")),
	}
}

func (h *Handler) ListJournals(w http.ResponseWriter, r *http.Request) {
	f := journalFilterFromQuery(r)
	list, total, err := h.svc.ListJournals(r.Context(), f)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	page, perPage := f.Page, f.PerPage
	if page < 1 {
		page = 1
	}
	if perPage < 1 || perPage > 100 {
		perPage = 20
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list, page, perPage, total)
}

func (h *Handler) GetJournal(w http.ResponseWriter, r *http.Request) {
	j, err := h.svc.GetJournal(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), j)
}

// JournalBySource handles GET /api/v1/accounting/journals/by-source — the
// drill-down from an upstream document (GR detail links its accrual entry).
func (h *Handler) JournalBySource(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	sourceType := q.Get("source_type")
	var sourceID int64
	fmt.Sscanf(q.Get("source_id"), "%d", &sourceID)
	if sourceType == "" || sourceID == 0 {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, "source_type dan source_id wajib diisi", nil)
		return
	}
	j, err := h.svc.JournalBySource(r.Context(), sourceType, sourceID)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), j)
}

func (h *Handler) CreateJournal(w http.ResponseWriter, r *http.Request) {
	var in JournalInput
	if !decode(w, r, &in) {
		return
	}
	j, err := h.svc.CreateJournal(r.Context(), in, callerID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), j)
}

func (h *Handler) UpdateJournal(w http.ResponseWriter, r *http.Request) {
	var in JournalInput
	if !decode(w, r, &in) {
		return
	}
	j, err := h.svc.UpdateJournal(r.Context(), pathID(r), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), j)
}

func (h *Handler) DeleteJournal(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeleteJournal(r.Context(), pathID(r)); err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), map[string]string{"message": "Jurnal draft berhasil dihapus"})
}

func (h *Handler) PostJournal(w http.ResponseWriter, r *http.Request) {
	j, err := h.svc.PostJournal(r.Context(), pathID(r), callerID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), j)
}

func (h *Handler) VoidJournal(w http.ResponseWriter, r *http.Request) {
	j, err := h.svc.VoidJournal(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), j)
}

func (h *Handler) ReverseJournal(w http.ResponseWriter, r *http.Request) {
	var in struct {
		Reason string `json:"reason"`
	}
	_ = decode(w, r, &in) // body optional; empty reason defaults in service
	j, err := h.svc.ReverseJournal(r.Context(), pathID(r), in.Reason, callerID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), j)
}

// --- general ledger -------------------------------------------------------------

func ledgerFilterFromQuery(r *http.Request) LedgerFilter {
	q := r.URL.Query()
	return LedgerFilter{
		DateFrom:    q.Get("date_from"),
		DateTo:      q.Get("date_to"),
		JournalType: q.Get("journal_type"),
		Status:      q.Get("status"),
		AccountCode: q.Get("account_code"),
		AccountType: q.Get("account_type"),
		Search:      q.Get("search"),
		StoreID:     queryInt64(q.Get("store_id")),
	}
}

func (h *Handler) Ledger(w http.ResponseWriter, r *http.Request) {
	f := ledgerFilterFromQuery(r)
	report, err := h.svc.Ledger(r.Context(), f)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	if r.URL.Query().Get("format") == "csv" {
		writeLedgerCSV(w, r, report)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), report)
}

func writeLedgerCSV(w http.ResponseWriter, r *http.Request, report *LedgerReport) {
	name := "ledger.csv"
	if report.DateFrom != "" || report.DateTo != "" {
		name = "ledger-" + report.DateFrom + "-" + report.DateTo + ".csv"
	}
	w.Header().Set("Content-Type", "text/csv; charset=utf-8")
	w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
	cw := csv.NewWriter(w)
	_ = cw.Write([]string{"account_code", "account_name", "account_type", "date", "journal_number", "journal_type", "status", "reference", "description", "debit", "credit", "balance"})
	for _, acc := range report.Accounts {
		balance := acc.OpeningBalance
		for _, e := range acc.Entries {
			balance = e.Balance
			_ = cw.Write([]string{
				acc.AccountCode, acc.AccountName, acc.AccountType,
				e.Date, e.JournalNumber, e.JournalType, e.Status, e.Reference, e.Description,
				e.Debit.StringFixed(2), e.Credit.StringFixed(2), balance.StringFixed(2),
			})
		}
	}
	_ = cw.Write([]string{"", "", "", "", "", "", "", "TOTAL", report.TotalDebit.StringFixed(2), report.TotalCredit.StringFixed(2), ""})
	cw.Flush()
}

// --- trial balance (mounted under /reports) --------------------------------------

func (h *Handler) TrialBalance(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	tb, err := h.svc.TrialBalance(r.Context(), q.Get("date_from"), q.Get("date_to"))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), tb)
}

// --- advances -----------------------------------------------------------------

func (h *Handler) ListAdvances(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := AdvanceFilter{
		Page:    queryInt(q.Get("page")),
		PerPage: queryInt(q.Get("per_page")),
		Type:    q.Get("type"),
		Status:  q.Get("status"),
		Search:  q.Get("search"),
	}
	list, total, err := h.svc.ListAdvances(r.Context(), f)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	page, perPage := f.Page, f.PerPage
	if page < 1 {
		page = 1
	}
	if perPage < 1 || perPage > 100 {
		perPage = 20
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list, page, perPage, total)
}

func (h *Handler) GetAdvance(w http.ResponseWriter, r *http.Request) {
	a, err := h.svc.GetAdvance(r.Context(), pathID(r), r.URL.Query().Get("type"))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), a)
}

func (h *Handler) ReceiveAdvance(w http.ResponseWriter, r *http.Request) {
	var in AdvanceReceiveInput
	if !decode(w, r, &in) {
		return
	}
	a, err := h.svc.ReceiveAdvance(r.Context(), in, callerID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), a)
}

func (h *Handler) AllocateAdvance(w http.ResponseWriter, r *http.Request) {
	var in AdvanceAllocateInput
	if !decode(w, r, &in) {
		return
	}
	a, err := h.svc.AllocateAdvance(r.Context(), pathID(r), r.URL.Query().Get("type"), in, callerID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), a)
}

func (h *Handler) RefundAdvance(w http.ResponseWriter, r *http.Request) {
	var in AdvanceRefundInput
	if !decode(w, r, &in) {
		return
	}
	a, err := h.svc.RefundAdvance(r.Context(), pathID(r), r.URL.Query().Get("type"), in, callerID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), a)
}
