package reports

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/TaruLab/keluargamart-backend/internal/db"
	"github.com/shopspring/decimal"
)

// Cash-account and classification rules for the cash-flow STATEMENT.
//
// WHY A STATEMENT AND NOT THE EXISTING ARRAY. GET /reports/cashflow is the
// generic RowCashflow export — a flat list of payments, purchase-order payments
// and manual entries. It has no notion of a journal, so it cannot classify a
// movement as operating/investing/financing, and it cannot produce an opening or
// closing balance. The ERP statement view used to read the three sections off
// that array anyway, found nothing, and rendered A = B = C = Rp 0 beside a net
// of Rp 30.108.506 — a statement whose total did not equal its own parts.
//
// WHY cashCodes IS AN EXPLICIT LIST, NOT `code LIKE '1%'`. This chart numbers
// receivables 110302, inventory 110401 and fixed assets 120101/120102. All four
// begin with "1" and none is cash. A prefix match would have folded receivables
// and inventory into a cash statement and produced a number that looked computed
// and meant nothing. Discovery reads the real account codes and falls back to
// this default only if the chart cannot be reached.
var defaultCashCodes = []string{
	"1000.01.01", // Kas Tunai Toko
	"1000.01.02", // Kas Kecil Operasional
	"1000.03.01", // Bank BCA Rekening Perusahaan
	"1000.03.02", // Bank Mandiri
}

// classifyCounterpart maps the non-cash side of a journal entry to a statement
// section, following PSAK 02's presentation:
//
//	operating  — day-to-day trading: 4xxx revenue, 5xxx/6xxx expense, and the
//	             current-asset/current-liability accounts that settlement lands in
//	             (receivables 11xxx, payables 2xxx short-term)
//	investing  — 12xxx/13xxx/14xxx/15xxx/16xxx fixed and long-term assets
//	financing  — 3xxx equity and 2xxx borrowings
//
// An unknown counterpart is OPERATING, not dropped. A cash movement that cannot
// be classified still happened and still moved the balance; hiding it would make
// the statement's net disagree with beginning + movement, which is the exact
// defect this endpoint exists to remove. It is still listed in items, so a reader
// can see what could not be attributed.
func classifyCounterpart(code string) string {
	switch {
	case strings.HasPrefix(code, "12"), strings.HasPrefix(code, "13"),
		strings.HasPrefix(code, "14"), strings.HasPrefix(code, "15"),
		strings.HasPrefix(code, "16"):
		return CashflowSectionInvesting
	case strings.HasPrefix(code, "21"):
		// Current liabilities: 210101 Hutang Usaha, 210102 Hutang Dagang Supplier, 2102 Accruals, 2103 Tax Payables
		// Under PSAK 02 / IAS 7, supplier debt payments are strictly Operating cash outflows.
		return CashflowSectionOperating
	case strings.HasPrefix(code, "3"), strings.HasPrefix(code, "22"), strings.HasPrefix(code, "23"), strings.HasPrefix(code, "24"):
		return CashflowSectionFinancing
	default:
		return CashflowSectionOperating
	}
}

func newSection(name string) CashflowStatementSection {
	return CashflowStatementSection{Section: name, Items: []CashflowStatementItem{}}
}

func (s *CashflowStatementSection) add(item CashflowStatementItem) {
	s.Items = append(s.Items, item)
	if item.Amount.Sign() >= 0 {
		s.Inflow = s.Inflow.Add(item.Amount)
	} else {
		s.Outflow = s.Outflow.Add(item.Amount.Abs())
	}
}

// finish recomputes Net from the classified items so the section can never
// disagree with its own rows.
func (s *CashflowStatementSection) finish() {
	s.Net = s.Inflow.Sub(s.Outflow)
}

func sectionByName(stmt *CashflowStatement, name string) *CashflowStatementSection {
	switch name {
	case CashflowSectionInvesting:
		return &stmt.Investing
	case CashflowSectionFinancing:
		return &stmt.Financing
	default:
		return &stmt.Operating
	}
}

// CashflowStatement builds the operating/investing/financing statement for a
// calendar year from journal_lines.
//
// Opening and closing balances come from a second pass over every journal line
// dated before the period start, so beginning + net is exactly ending by
// construction rather than by coincidence.
func (s *Service) CashflowStatement(ctx context.Context, year int) (*CashflowStatement, error) {
	loc := s.loc
	from := time.Date(year, time.January, 1, 0, 0, 0, 0, loc)
	to := time.Date(year+1, time.January, 1, 0, 0, 0, 0, loc)

	codes, err := s.resolveCashCodes(ctx)
	if err != nil {
		return nil, err
	}

	stmt := &CashflowStatement{
		Year:         year,
		PeriodFrom:   from.Format("2006-01-02"),
		PeriodTo:     to.AddDate(0, 0, -1).Format("2006-01-02"),
		CashAccounts: codes,
		Operating:    newSection(CashflowSectionOperating),
		Investing:    newSection(CashflowSectionInvesting),
		Financing:    newSection(CashflowSectionFinancing),
	}

	rows, err := s.store.CashflowStatementMovements(ctx, db.CashflowStatementMovementsParams{
		CashCodes: codes,
		DateFrom:  pgdate(from),
		DateTo:    pgdate(to),
	})
	if err != nil {
		return nil, err
	}

	journals := map[string]struct{}{}
	for _, r := range rows {
		// A transfer between two of our own cash accounts is not an inflow or
		// an outflow of the business; it is the same money moving. Both legs
		// are dropped so the statement does not report a phantom movement.
		if isCashCode(r.CounterpartCode) {
			continue
		}
		sign := decimal.NewFromInt(int64(r.CreditSign))
		if sign.Sign() == 0 {
			continue
		}
		item := CashflowStatementItem{
			JournalNumber:   r.JournalNumber,
			TxDate:          r.TxDate.Time.Format("2006-01-02"),
			CashAccount:     r.CashCode,
			CashAccountName: r.CashName,
			CounterpartCode: r.CounterpartCode,
			CounterpartName: r.CounterpartName,
			Description:     cashflowDescription(r.CounterpartName, r.CashName),
			Amount:          r.Amount.Mul(sign),
		}
		sectionByName(stmt, classifyCounterpart(r.CounterpartCode)).add(item)
		journals[r.JournalNumber] = struct{}{}
	}

	stmt.Operating.finish()
	stmt.Investing.finish()
	stmt.Financing.finish()
	stmt.NetCashFlow = stmt.Operating.Net.Add(stmt.Investing.Net).Add(stmt.Financing.Net)
	stmt.JournalCount = len(journals)

	opening, err := s.cashBalanceBefore(ctx, codes, from)
	if err != nil {
		return nil, err
	}
	stmt.BeginningCash = opening
	stmt.EndingCash = opening.Add(stmt.NetCashFlow)
	return stmt, nil
}

func isCashCode(code string) bool {
	return strings.HasPrefix(code, "1000.")
}

func cashflowDescription(counterpart, cash string) string {
	switch {
	case counterpart != "" && cash != "":
		return fmt.Sprintf("%s — %s", counterpart, cash)
	case counterpart != "":
		return counterpart
	case cash != "":
		return cash
	default:
		return "Mutasi kas"
	}
}

// resolveCashCodes returns the cash account codes, preferring the ones actually
// in the chart so a new bank account is picked up without a code change.
func (s *Service) resolveCashCodes(ctx context.Context) ([]string, error) {
	rows, err := s.store.CashAccountCodes(ctx)
	if err != nil {
		return nil, err
	}
	if len(rows) == 0 {
		return defaultCashCodes, nil
	}
	out := make([]string, 0, len(rows))
	for _, code := range rows {
		if isCashCode(code) {
			out = append(out, code)
		}
	}
	if len(out) == 0 {
		return defaultCashCodes, nil
	}
	sort.Strings(out)
	return out, nil
}

// cashBalanceBefore sums every cash-account line dated before the period start.
// Debits increase cash, credits decrease it.
func (s *Service) cashBalanceBefore(ctx context.Context, codes []string, before time.Time) (decimal.Decimal, error) {
	rows, err := s.store.CashAccountBalanceBefore(ctx, db.CashAccountBalanceBeforeParams{
		CashCodes: codes,
		Before:    pgdate(before),
	})
	if err != nil {
		return decimal.Zero, err
	}
	return rows, nil
}
