// Package finance implements invoicing (fakturisasi) + Arus Kas (cashflow)
// (business-rules §5-§6). Invoice create from confirmed/delivered SO with
// subtotal/tax left 0.00 (parity); invoice pay writes Payment only — cashflow
// entries are manual (Arus Kas Kasir); receivables computes overdue; cashflow
// list filters type/category/date with type-summary. Codes: PAY/CF-YYYYMMDD-<4> (§1).
package finance

import (
	"context"
	"time"

	"github.com/shopspring/decimal"

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

// Invoice mirrors invoices.
type Invoice struct {
	ID              int64           `json:"id"`
	InvoiceNumber   string          `json:"invoice_number"`
	SalesOrderID    int64           `json:"sales_order_id"`
	CustomerID      int64           `json:"customer_id"`
	InvoiceDate     string          `json:"invoice_date"`
	DueDate         string          `json:"due_date"`
	Status          string          `json:"status"`
	Subtotal        decimal.Decimal `json:"subtotal"`
	TaxAmount       decimal.Decimal `json:"tax_amount"`
	TotalAmount     decimal.Decimal `json:"total_amount"`
	PaidAmount      decimal.Decimal `json:"paid_amount"`
	RemainingAmount decimal.Decimal `json:"remaining_amount"`
	Notes           *string         `json:"notes"`
	CustomerName    string          `json:"customer_name,omitempty"`
	SoNumber        string          `json:"so_number,omitempty"`
	StoreID         *int64          `json:"store_id,omitempty"`
	StoreName       string          `json:"store_name,omitempty"`
	OrderType       string          `json:"order_type,omitempty"`
	// SnapOrderRef is the Midtrans Snap order_id (migration 000095). It links
	// webhook order_id back to the invoice and is the settle idempotency key.
	SnapOrderRef *string `json:"snap_order_ref,omitempty"`
	// Items are the source sales order's lines (there is no invoice_items
	// table). Populated by the detail view only; list rows stay lean.
	Items     []InvoiceItem `json:"items,omitempty"`
	CreatedAt time.Time     `json:"created_at"`
	UpdatedAt time.Time     `json:"updated_at"`
}

// InvoiceItem is one line of an invoice, resolved from the source sales
// order's items through invoices.sales_order_id.
type InvoiceItem struct {
	ID              int64           `json:"id"`
	SalesOrderID    int64           `json:"sales_order_id"`
	ProductID       int64           `json:"product_id"`
	ProductCode     string          `json:"product_code"`
	ProductName     string          `json:"product_name"`
	Quantity        decimal.Decimal `json:"quantity"`
	UnitPrice       decimal.Decimal `json:"unit_price"`
	DiscountPercent decimal.Decimal `json:"discount_percent"`
	DiscountAmount  decimal.Decimal `json:"discount_amount"`
	Subtotal        decimal.Decimal `json:"subtotal"`
}

// InvoiceFilter carries pagination/search for the invoice list. PayableID and
// PayableType scope the payments list to one morph payable.
type InvoiceFilter struct {
	Page        int
	PerPage     int
	Search      string
	Status      string
	CustomerID  int64
	StoreID     int64
	DateFrom    string
	DateTo      string
	PayableID   int64
	PayableType string
	SortBy      string
	SortDir     string
}

// ReceivableRow is one row of the receivables list (§2.7).
type ReceivableRow struct {
	InvoiceID       int64           `json:"invoice_id"`
	InvoiceNumber   string          `json:"invoice_number"`
	CustomerID      int64           `json:"customer_id"`
	CustomerName    string          `json:"customer_name"`
	CustomerPhone   *string         `json:"customer_phone"`
	CustomerAddress string          `json:"customer_address"`
	InvoiceDate     string          `json:"invoice_date"`
	DueDate         string          `json:"due_date"`
	Status          string          `json:"status"`
	SoNumber        string          `json:"so_number"`
	PaymentTerms    *int32          `json:"payment_terms"`
	SalesName       *string         `json:"sales_name"`
	StoreID         *int64          `json:"store_id,omitempty"`
	StoreName       string          `json:"store_name,omitempty"`
	OrderType       string          `json:"order_type,omitempty"`
	TotalAmount     decimal.Decimal `json:"total_amount"`
	PaidAmount      decimal.Decimal `json:"paid_amount"`
	Remaining       decimal.Decimal `json:"remaining_amount"`
	Overdue         bool            `json:"overdue"`
	OverdueDays     int64           `json:"overdue_days"`
}

// ReceivableFilter carries filters for receivables.
type ReceivableFilter struct {
	Page       int
	PerPage    int
	CustomerID int64
	StoreID    int64
	Status     string
}

// Payment mirrors payments (morph payable).
type Payment struct {
	ID              int64           `json:"id"`
	PaymentNumber   string          `json:"payment_number"`
	PayableType     string          `json:"payable_type"`
	PayableID       int64           `json:"payable_id"`
	PaymentDate     string          `json:"payment_date"`
	Amount          decimal.Decimal `json:"amount"`
	PaymentMethod   string          `json:"payment_method"`
	BankName        *string         `json:"bank_name"`
	BankAccount     *string         `json:"bank_account"`
	ReferenceNumber *string         `json:"reference_number"`
	Notes           *string         `json:"notes"`
	ProofURL        *string         `json:"proof_url"`
	CreatedBy       *int64          `json:"created_by"`
	PayableLabel    string          `json:"payable_label,omitempty"`
	CustomerName    string          `json:"customer_name,omitempty"`
	StoreID         *int64          `json:"store_id,omitempty"`
	StoreName       string          `json:"store_name,omitempty"`
	OrderType       string          `json:"order_type,omitempty"`
}

// PartialPaymentInput mirrors an additional-payment request body
// (POST /payments/{id}/partial). The payable is taken from the referenced
// payment, not from the request.
type PartialPaymentInput struct {
	Amount        decimal.Decimal `json:"amount"`
	PaymentMethod string          `json:"payment_method"`
	Notes         *string         `json:"notes"`
}

// PaymentInput mirrors a payment request body (§5.2).
type PaymentInput struct {
	Amount          decimal.Decimal `json:"amount"`
	PaymentMethod   string          `json:"payment_method"`
	BankName        *string         `json:"bank_name"`
	BankAccount     *string         `json:"bank_account"`
	ReferenceNumber *string         `json:"reference_number"`
	Notes           *string         `json:"notes"`
}

// Cashflow mirrors cashflows.
type Cashflow struct {
	ID                int64           `json:"id"`
	TransactionNumber string          `json:"transaction_number"`
	Type              string          `json:"type"`
	Category          string          `json:"category"`
	TransactionDate   string          `json:"transaction_date"`
	Amount            decimal.Decimal `json:"amount"`
	Description       *string         `json:"description"`
	ReferenceType     *string         `json:"reference_type"`
	ReferenceID       *int64          `json:"reference_id"`
	ReversalOf        *int64          `json:"reversal_of"`
	CreatedBy         *int64          `json:"created_by"`
	// Accounting hardening (client feedback 2026-08-26 section 7).
	AccountID             *int64  `json:"account_id"`
	CounterpartAccountID  *int64  `json:"counterpart_account_id"`
	FlowType              *string `json:"flow_type"`
	PaymentMethod         *string `json:"payment_method"`
	AttachmentKey         *string `json:"attachment_key"`
	AttachmentFilename    *string `json:"attachment_filename"`
	AttachmentContentType *string `json:"attachment_content_type"`
	AttachmentSize        *int64  `json:"attachment_size"`
	AttachmentUploadedBy  *int64  `json:"attachment_uploaded_by"`
	AttachmentUploadedAt  *string `json:"attachment_uploaded_at"`
	// StoreID is the outlet the row belongs to (migration 000104). A pointer
	// because NULL is a MEANINGFUL value, not an absence: it marks a
	// company-level row, which is visible only to a platform-wide caller. A
	// plain int64 would render company-level as outlet 0 and lose the
	// distinction in the response.
	StoreID *int64 `json:"store_id"`
}

// CashflowFilter carries filters for the cashflow list (§6).
type CashflowFilter struct {
	Page     int
	PerPage  int
	Type     string
	Category string
	DateFrom string
	DateTo   string
	// StoreIDs is the CALLER'S OUTLET SCOPE, set by the service from
	// auth.StoreScopeFrom — never from the request, so a ?store_id= parameter
	// cannot widen it.
	//
	// nil and empty are DIFFERENT and both matter:
	//   - nil   = platform-wide; the SQL matches every row including
	//     company-level (store_id IS NULL) rows.
	//   - empty = a restricted scope resolving to no stores; the SQL matches
	//     nothing. Fail closed, by a different route than cashflowStoreScope's
	//     refusal but with the same outcome.
	StoreIDs []int64
}

// CashflowSummary is the SUM(amount) GROUP BY type within the filter (§6).
type CashflowSummary struct {
	Income  decimal.Decimal `json:"income"`
	Expense decimal.Decimal `json:"expense"`
}

// CashflowInput mirrors a manual cashflow create body (§6).
type CashflowInput struct {
	Type            string          `json:"type"`
	Category        string          `json:"category"`
	TransactionDate string          `json:"transaction_date"`
	Amount          decimal.Decimal `json:"amount"`
	Description     *string         `json:"description"`
	// Accounting hardening (client feedback 2026-08-26 section 7): manual
	// cash movements MUST name the cash/bank account they move and the COA
	// counterpart they hit. Flow type classifies the statement activity.
	AccountID             *int64  `json:"account_id"`
	CounterpartAccountID  *int64  `json:"counterpart_account_id"`
	FlowType              *string `json:"flow_type"`
	PaymentMethod         *string `json:"payment_method"`
	AttachmentKey         *string `json:"attachment_key"`
	AttachmentFilename    *string `json:"attachment_filename"`
	AttachmentContentType *string `json:"attachment_content_type"`
	AttachmentSize        *int64  `json:"attachment_size"`
}

// CashflowReverseInput carries the reason for a generated-cashflow reversal.
type CashflowReverseInput struct {
	Reason *string `json:"reason"`
}

// SOHeader is the minimal sales_orders row needed to invoice (§5.1).
type SOHeader struct {
	ID          int64           `json:"id"`
	SoNumber    string          `json:"so_number"`
	CustomerID  int64           `json:"customer_id"`
	Status      string          `json:"status"`
	TotalAmount decimal.Decimal `json:"total_amount"`
}

// 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
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

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

	// invoices
	CreateInvoice(ctx context.Context, q db.DBTX, p db.CreateInvoiceParams) (db.CreateInvoiceRow, error)
	UpdateInvoice(ctx context.Context, q db.DBTX, p db.UpdateInvoiceParams) error
	InvoiceByID(ctx context.Context, id int64) (db.GetInvoiceByIDRow, error)
	InvoiceItems(ctx context.Context, salesOrderID int64) ([]db.ListInvoiceItemsRow, error)
	LockInvoice(ctx context.Context, q db.DBTX, id int64) (db.GetInvoiceByIDForUpdateRow, error)
	ListInvoices(ctx context.Context, f InvoiceFilter) ([]db.ListInvoicesRow, int, error)
	LockSO(ctx context.Context, q db.DBTX, id int64) (db.GetSOHeaderByIDForUpdateRow, error)
	InvoiceExistsForSO(ctx context.Context, q db.DBTX, soID int64) (bool, error)
	SetSOStatus(ctx context.Context, q db.DBTX, p db.SetSOStatusParams) error
	InsertSOStatusHistory(ctx context.Context, q db.DBTX, p db.InsertSOStatusHistoryParams) error
	InvoiceCreationStatusHistory(ctx context.Context, q db.DBTX, p db.GetInvoiceCreationStatusHistoryForUpdateParams) (db.SalesOrderStatusHistory, error)
	PaymentsExistForInvoice(ctx context.Context, q db.DBTX, invoiceID int64) (bool, error)
	DeleteInvoice(ctx context.Context, q db.DBTX, id int64) error
	ListReceivables(ctx context.Context, f ReceivableFilter) ([]db.ListReceivablesRow, int, decimal.Decimal, error)

	// payments
	CreatePayment(ctx context.Context, q db.DBTX, p db.CreatePaymentParams) (int64, error)
	ListPayments(ctx context.Context, f InvoiceFilter) ([]db.ListPaymentsRow, int, error)
	// SumCustomerPayments returns the buyer's payment total + row count in one
	// indexed scan — the fast path behind MyPayments (no LEFT-join fan-out).
	SumCustomerPayments(ctx context.Context, customerID int64, dateFrom, dateTo string) (db.SumCustomerPaymentsRow, error)
	GetPaymentByID(ctx context.Context, id int64) (db.GetPaymentByIDForUpdateRow, error)
	LockPayment(ctx context.Context, q db.DBTX, id int64) (db.GetPaymentByIDForUpdateRow, error)
	SetPaymentProofURL(ctx context.Context, id int64, url string) (int64, error)
	// SetPaymentReview stamps the VerifyPayment verdict onto the payment row
	// (notes audit). Rows-affected is returned so callers can 404 unknown ids.
	SetPaymentReview(ctx context.Context, id int64, verdict string) (int64, error)
	// PaymentProofOwnersByKey resolves which users own the payment proof stored
	// under key (payment -> invoice -> customers.user_id). found=false means no
	// payment references the key, i.e. the object is not a payment proof. The
	// payments table is the source of truth so proofs stored under legacy
	// single-segment keys are covered too.
	PaymentProofOwnersByKey(ctx context.Context, key string) (owners []int64, found bool, err error)

	// cashflows
	CreateCashflow(ctx context.Context, q db.DBTX, p db.CreateCashflowParams) (int64, error)
	// CashflowByID and LockCashflow take the caller's outlet scope: nil is
	// platform-wide, a non-nil slice restricts to those stores and excludes
	// company-level rows. Both are scoped because both are reachable with a
	// caller-supplied id, and the lock one is reachable by the mutation paths
	// as well.
	CashflowByID(ctx context.Context, id int64, storeIDs []int64) (db.GetCashflowByIDRow, error)
	LockCashflow(ctx context.Context, q db.DBTX, id int64, storeIDs []int64) (db.GetCashflowByIDForUpdateRow, error)
	CashflowAlreadyReversed(ctx context.Context, q db.DBTX, id int64) (bool, error)
	UpdateCashflow(ctx context.Context, q db.DBTX, p db.UpdateCashflowParams) (db.UpdateCashflowRow, error)
	DeleteCashflow(ctx context.Context, q db.DBTX, id int64) error
	ListCashflows(ctx context.Context, f CashflowFilter) ([]db.ListCashflowsRow, int, db.SummarizeCashflowsRow, error)
}

type AccountingPeriodLog struct {
	ID          int64          `json:"id"`
	PeriodYear  int16          `json:"period_year"`
	PeriodMonth int16          `json:"period_month"`
	Action      string         `json:"action"`
	ActorID     int64          `json:"actor_id"`
	ActorName   string         `json:"actor_name,omitempty"`
	Reason      *string        `json:"reason,omitempty"`
	Snapshot    map[string]any `json:"snapshot,omitempty"`
	CreatedAt   time.Time      `json:"created_at"`
}

type PeriodValidationResult struct {
	CanClose bool          `json:"can_close"`
	Issues   []string      `json:"issues"`
	Summary  PeriodSummary `json:"summary"`
}

type AccountingPeriod struct {
	ID            int64                 `json:"id"`
	PeriodYear    int16                 `json:"period_year"`
	PeriodMonth   int16                 `json:"period_month"`
	Status        string                `json:"status"`
	LockedAt      *time.Time            `json:"locked_at,omitempty"`
	LockedBy      *int64                `json:"locked_by,omitempty"`
	ReopenedAt    *time.Time            `json:"reopened_at,omitempty"`
	ReopenedBy    *int64                `json:"reopened_by,omitempty"`
	ReopenReason  *string               `json:"reopen_reason,omitempty"`
	CreatedAt     time.Time             `json:"created_at"`
	UpdatedAt     time.Time             `json:"updated_at"`
	StartDate     string                `json:"start_date"`
	EndDate       string                `json:"end_date"`
	CashflowIn    decimal.Decimal       `json:"cashflow_in"`
	CashflowOut   decimal.Decimal       `json:"cashflow_out"`
	CashflowCount int64                 `json:"cashflow_count"`
	Snapshot      map[string]any        `json:"snapshot,omitempty"`
	History       []AccountingPeriodLog `json:"history,omitempty"`
}

type PeriodSummary struct {
	CashflowIn       decimal.Decimal `json:"cashflow_in"`
	CashflowOut      decimal.Decimal `json:"cashflow_out"`
	NetCashflow      decimal.Decimal `json:"net_cashflow"`
	CashflowCount    int64           `json:"cashflow_count"`
	SalesCount       int64           `json:"sales_count"`
	SalesTotal       decimal.Decimal `json:"sales_total"`
	JournalCount     int64           `json:"journal_count"`
	JournalDebit     decimal.Decimal `json:"journal_debit"`
	JournalCredit    decimal.Decimal `json:"journal_credit"`
	UnpostedJournals int64           `json:"unposted_journals"`
	IsBalanced       bool            `json:"is_balanced"`
}

type periodStore interface {
	ListAccountingPeriods(context.Context) ([]AccountingPeriod, error)
	AccountingPeriod(context.Context, int16, int16) (AccountingPeriod, error)
	AccountingPeriodSummary(context.Context, int16, int16) (PeriodSummary, error)
	ValidateAccountingPeriod(context.Context, int16, int16) (PeriodValidationResult, error)
	CloseAccountingPeriod(context.Context, int16, int16, int64) (AccountingPeriod, error)
	ReopenAccountingPeriod(context.Context, int16, int16, int64, string) (AccountingPeriod, error)
	AccountingPeriodHistory(context.Context, int16, int16) ([]AccountingPeriodLog, error)
}
