// Package accounting implements the /api/v1/accounting surface: chart of
// accounts, cost-center departments, double-entry journals (draft -> post /
// void / reverse), the general ledger, and customer deposits / supplier
// advances. Every advance mutation writes an advance_transactions row and
// an auto-posted journal inside one DB transaction. Amounts are
// NUMERIC(15,2) via shopspring/decimal (quoted strings on the wire).
package accounting

import (
	"context"
	"encoding/json"
	"strconv"
	"strings"
	"time"

	"github.com/shopspring/decimal"

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

// Account is one chart-of-accounts row (extends the 000041 accounts table).
type Account struct {
	ID            int64     `json:"id"`
	Code          string    `json:"code"`
	Name          string    `json:"name"`
	Type          string    `json:"type"`
	ParentID      *int64    `json:"parent_id"`
	NormalBalance string    `json:"normal_balance"`
	IsPostable    bool      `json:"is_postable"`
	IsPlaceholder bool      `json:"is_placeholder"`
	IsActive      bool      `json:"is_active"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
}

// AccountInput mirrors a create/update account body.
type AccountInput struct {
	Code          string `json:"code"`
	Name          string `json:"name"`
	Type          string `json:"type"`
	NormalBalance string `json:"normal_balance"`
	ParentID      *int64 `json:"parent_id"`
	IsPostable    *bool  `json:"is_postable"`
	IsPlaceholder *bool  `json:"is_placeholder"`
	IsActive      *bool  `json:"is_active"`
}

// Department is a cost center required on every journal line.
type Department struct {
	ID        int64     `json:"id"`
	Code      string    `json:"code"`
	Name      string    `json:"name"`
	IsActive  bool      `json:"is_active"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

// DepartmentInput mirrors a create/update department body.
type DepartmentInput struct {
	Code     string `json:"code"`
	Name     string `json:"name"`
	IsActive *bool  `json:"is_active"`
}

// JournalLine is one debit/credit line of a journal entry.
type JournalLine struct {
	ID             int64           `json:"id"`
	AccountCode    string          `json:"account_code"`
	AccountName    string          `json:"account_name"`
	DepartmentCode string          `json:"department_code"`
	Debit          decimal.Decimal `json:"debit"`
	Credit         decimal.Decimal `json:"credit"`
	Description    string          `json:"description"`
}

// JournalLineInput mirrors one line of a create/update journal body.
type JournalLineInput struct {
	AccountCode    string          `json:"account_code"`
	DepartmentCode string          `json:"department_code"`
	Debit          decimal.Decimal `json:"debit"`
	Credit         decimal.Decimal `json:"credit"`
	Description    string          `json:"description"`
}

// Journal is one journal entry with its lines.
type Journal struct {
	ID              int64           `json:"id"`
	JournalNumber   string          `json:"journal_number"`
	JournalType     string          `json:"journal_type"`
	EntryDate       string          `json:"entry_date"`
	Reference       string          `json:"reference"`
	Description     string          `json:"description"`
	Status          string          `json:"status"`
	StoreID         *int64          `json:"store_id"`
	TotalDebit      decimal.Decimal `json:"total_debit"`
	TotalCredit     decimal.Decimal `json:"total_credit"`
	CreatedAt       time.Time       `json:"created_at"`
	PostedAt        *time.Time      `json:"posted_at"`
	ReversedAt      *time.Time      `json:"reversed_at"`
	ReversalEntryID *int64          `json:"reversal_entry_id"`
	SourceType      string          `json:"source_type,omitempty"`
	SourceID        *int64          `json:"source_id,omitempty"`
	Lines           []JournalLine   `json:"lines"`
}

// JournalInput mirrors a create/update journal body.
type JournalInput struct {
	JournalType string             `json:"journal_type"`
	EntryDate   string             `json:"entry_date"`
	Reference   string             `json:"reference"`
	Description string             `json:"description"`
	StoreID     *int64             `json:"store_id"`
	// SourceType/SourceID link an entry to its upstream document
	// (e.g. goods_receipt, pembelian, hutang_outpost).
	SourceType string             `json:"source_type,omitempty"`
	SourceID   *int64             `json:"source_id,omitempty"`
	Lines      []JournalLineInput `json:"lines"`
}

// UnmarshalJSON tolerates string, numeric, null, or empty string values for store_id and source_id
// to prevent UnmarshalTypeError when clients/browsers serialize select option values.
func (in *JournalInput) UnmarshalJSON(data []byte) error {
	type Alias JournalInput
	var aux struct {
		Alias
		RawStoreID  any `json:"store_id"`
		RawSourceID any `json:"source_id"`
	}
	if err := json.Unmarshal(data, &aux); err != nil {
		return err
	}
	*in = JournalInput(aux.Alias)
	in.StoreID = parseOptionalInt64(aux.RawStoreID)
	in.SourceID = parseOptionalInt64(aux.RawSourceID)
	return nil
}

func parseOptionalInt64(v any) *int64 {
	if v == nil {
		return nil
	}
	switch val := v.(type) {
	case float64:
		n := int64(val)
		return &n
	case int64:
		return &val
	case int:
		n := int64(val)
		return &n
	case string:
		trimmed := strings.TrimSpace(val)
		if trimmed == "" || trimmed == "null" || trimmed == "undefined" {
			return nil
		}
		if n, err := strconv.ParseInt(trimmed, 10, 64); err == nil {
			return &n
		}
	case json.Number:
		if n, err := val.Int64(); err == nil {
			return &n
		}
	}
	return nil
}

// JournalFilter carries the journal list filters (JournalsPage).
type JournalFilter struct {
	Page                int
	PerPage             int
	JournalType         string
	AllowedJournalTypes []string
	Status              string
	Search              string
	DateFrom            string
	DateTo              string
	StoreID             int64
	AllowedStoreIDs     []int64
}

// LedgerEntry is one movement on one account, with running balance and status.
type LedgerEntry struct {
	ID            int64           `json:"id"`
	EntryID       int64           `json:"entry_id"`
	Date          string          `json:"date"`
	JournalNumber string          `json:"journal_number"`
	JournalType   string          `json:"journal_type"`
	Status        string          `json:"status"`
	Reference     string          `json:"reference"`
	Description   string          `json:"description"`
	Debit         decimal.Decimal `json:"debit"`
	Credit        decimal.Decimal `json:"credit"`
	Balance       decimal.Decimal `json:"balance"`
}

// LedgerAccount groups the entries of one account with opening/ending balance.
type LedgerAccount struct {
	AccountID      int64           `json:"account_id"`
	AccountCode    string          `json:"account_code"`
	AccountName    string          `json:"account_name"`
	AccountType    string          `json:"account_type"`
	NormalBalance  string          `json:"normal_balance"`
	OpeningBalance decimal.Decimal `json:"opening_balance"`
	EndingBalance  decimal.Decimal `json:"ending_balance"`
	Entries        []LedgerEntry   `json:"entries"`
}

// LedgerReport is the GET /accounting/ledger payload.
type LedgerReport struct {
	DateFrom    string          `json:"date_from"`
	DateTo      string          `json:"date_to"`
	Accounts    []LedgerAccount `json:"accounts"`
	TotalDebit  decimal.Decimal `json:"total_debit"`
	TotalCredit decimal.Decimal `json:"total_credit"`
}

// LedgerFilter carries the ledger filters (GeneralLedgerPage).
type LedgerFilter struct {
	DateFrom        string
	DateTo          string
	JournalType     string
	Status          string
	AccountCode     string
	AccountType     string
	Search          string
	StoreID         int64
	AllowedStoreIDs []int64
}

// TrialBalanceRow is one account's debit/credit totals.
type TrialBalanceRow struct {
	AccountID     int64           `json:"account_id"`
	AccountCode   string          `json:"account_code"`
	AccountName   string          `json:"account_name"`
	AccountType   string          `json:"account_type"`
	NormalBalance string          `json:"normal_balance"`
	Debit         decimal.Decimal `json:"debit"`
	Credit        decimal.Decimal `json:"credit"`
}

// TrialBalance is the GET /reports/trial-balance payload.
type TrialBalance struct {
	DateFrom    string            `json:"date_from"`
	DateTo      string            `json:"date_to"`
	Accounts    []TrialBalanceRow `json:"accounts"`
	TotalDebit  decimal.Decimal   `json:"total_debit"`
	TotalCredit decimal.Decimal   `json:"total_credit"`
}

// AdvanceTxn is one receipt/allocation/refund on an advance.
type AdvanceTxn struct {
	ID             int64           `json:"id"`
	TxnType        string          `json:"txn_type"`
	Amount         decimal.Decimal `json:"amount"`
	Reference      string          `json:"reference"`
	JournalEntryID *int64          `json:"journal_entry_id"`
	Notes          string          `json:"notes"`
	CreatedAt      time.Time       `json:"created_at"`
}

// Advance is one customer deposit / supplier advance. Amount aliases
// (allocated/refunded/remainder, date/advance_date) keep both the mock DTO
// and the AdvancesPage table columns working off the same row.
type Advance struct {
	ID              int64           `json:"id"`
	AdvanceNumber   string          `json:"advance_number"`
	AdvanceType     string          `json:"advance_type"`
	CustomerID      *int64          `json:"customer_id"`
	CustomerName    string          `json:"customer_name"`
	SupplierID      *int64          `json:"supplier_id"`
	SupplierName    string          `json:"supplier_name"`
	Amount          decimal.Decimal `json:"amount"`
	AllocatedAmount decimal.Decimal `json:"allocated_amount"`
	RefundedAmount  decimal.Decimal `json:"refunded_amount"`
	RemainingAmount decimal.Decimal `json:"remaining_amount"`
	Allocated       decimal.Decimal `json:"allocated"`
	Refunded        decimal.Decimal `json:"refunded"`
	Remainder       decimal.Decimal `json:"remainder"`
	Status          string          `json:"status"`
	Date            string          `json:"date"`
	AdvanceDate     string          `json:"advance_date"`
	Notes           string          `json:"notes"`
	Transactions    []AdvanceTxn    `json:"transactions,omitempty"`
}

// AdvanceFilter carries the advance list filters (AdvancesPage).
type AdvanceFilter struct {
	Page    int
	PerPage int
	Type    string
	Status  string
	Search  string
}

// AdvanceReceiveInput mirrors the receive-deposit body.
type AdvanceReceiveInput struct {
	Type            string          `json:"type"`
	AdvanceDate     string          `json:"advance_date"`
	Amount          decimal.Decimal `json:"amount"`
	CashAccountCode string          `json:"cash_account_code"`
	Notes           string          `json:"notes"`
	CustomerID      *int64          `json:"customer_id"`
	SupplierID      *int64          `json:"supplier_id"`
}

// AdvanceAllocateInput mirrors the allocate body.
type AdvanceAllocateInput struct {
	Amount          decimal.Decimal `json:"amount"`
	InvoiceID       *int64          `json:"invoice_id"`
	PurchaseOrderID *int64          `json:"purchase_order_id"`
}

// AdvanceRefundInput mirrors the refund body.
type AdvanceRefundInput struct {
	Amount          decimal.Decimal `json:"amount"`
	CashAccountCode string          `json:"cash_account_code"`
}

// Tx is a physical transaction; pgx.Tx satisfies it. It doubles as the
// db.DBTX handed to tx-scoped store writes.
type Tx interface {
	db.DBTX
	Rollback(ctx context.Context) error
	Commit(ctx context.Context) error
}

// Store is the accounting persistence seam (sqlc-backed, repository.go).
type Store interface {
	Begin(ctx context.Context) (Tx, error)

	ListAccounts(ctx context.Context, active *bool, search string) ([]db.Account, error)
	GetAccountByCode(ctx context.Context, q db.DBTX, code string) (db.Account, error)
	CreateAccount(ctx context.Context, p db.AcctCreateAccountParams) (db.Account, error)
	UpdateAccount(ctx context.Context, p db.AcctUpdateAccountParams) (db.Account, error)

	ListDepartments(ctx context.Context, active *bool, search string) ([]db.AccountingDepartment, error)
	GetDepartmentByCode(ctx context.Context, q db.DBTX, code string) (db.AccountingDepartment, error)
	CreateDepartment(ctx context.Context, p db.AcctCreateDepartmentParams) (db.AccountingDepartment, error)
	UpdateDepartment(ctx context.Context, p db.AcctUpdateDepartmentParams) (db.AccountingDepartment, error)

	CountJournals(ctx context.Context, f JournalFilter) (int, error)
	ListJournals(ctx context.Context, f JournalFilter) ([]db.JournalEntry, error)
	ListJournalLinesByEntries(ctx context.Context, entryIDs []int64) ([]db.AcctListJournalLinesByEntriesRow, error)
	GetJournal(ctx context.Context, id int64) (db.JournalEntry, error)
	JournalBySource(ctx context.Context, sourceType string, sourceID int64) (db.JournalEntry, error)
	LockJournal(ctx context.Context, q db.DBTX, id int64) (db.JournalEntry, error)
	CreateJournal(ctx context.Context, q db.DBTX, p db.AcctCreateJournalParams) (db.JournalEntry, error)
	UpdateJournal(ctx context.Context, q db.DBTX, p db.AcctUpdateJournalParams) error
	SetJournalPosted(ctx context.Context, q db.DBTX, p db.AcctSetJournalPostedParams) error
	SetJournalVoided(ctx context.Context, q db.DBTX, id int64) error
	SetJournalReversed(ctx context.Context, q db.DBTX, p db.AcctSetJournalReversedParams) error
	DeleteJournal(ctx context.Context, q db.DBTX, id int64) error
	CreateJournalLine(ctx context.Context, q db.DBTX, p db.AcctCreateJournalLineParams) (int64, error)
	DeleteJournalLines(ctx context.Context, q db.DBTX, entryID int64) error

	LedgerLines(ctx context.Context, f LedgerFilter) ([]db.AcctLedgerLinesRow, error)
	LedgerOpening(ctx context.Context, f LedgerFilter) ([]db.AcctLedgerOpeningRow, error)
	TrialBalanceRows(ctx context.Context, dateFrom, dateTo string) ([]db.AcctTrialBalanceRow, error)

	CountAdvances(ctx context.Context, f AdvanceFilter) (int, error)
	ListAdvances(ctx context.Context, f AdvanceFilter) ([]db.AcctListAdvancesRow, error)
	GetAdvance(ctx context.Context, id int64) (db.AcctGetAdvanceRow, error)
	LockAdvance(ctx context.Context, q db.DBTX, id int64) (db.Advance, error)
	CreateAdvance(ctx context.Context, q db.DBTX, p db.AcctCreateAdvanceParams) (db.Advance, error)
	SetAdvanceAmounts(ctx context.Context, q db.DBTX, p db.AcctSetAdvanceAmountsParams) error
	CreateAdvanceTxn(ctx context.Context, q db.DBTX, p db.AcctCreateAdvanceTxnParams) (db.AdvanceTransaction, error)
	ListAdvanceTxns(ctx context.Context, advanceID int64) ([]db.AdvanceTransaction, error)
}
