package accounting

import (
	"context"
	"errors"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"

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

// Repository implements Store on a pgx pool.
type Repository struct {
	pool *pgxpool.Pool
	q    *db.Queries
}

func NewRepository(pool *pgxpool.Pool) *Repository {
	return &Repository{pool: pool, q: db.New(pool)}
}

func (r *Repository) Begin(ctx context.Context) (Tx, error) {
	return r.pool.Begin(ctx)
}

func mapErr(err error) error {
	if errors.Is(err, pgx.ErrNoRows) {
		return ErrNotFound
	}
	var pgErr *pgconn.PgError
	if errors.As(err, &pgErr) {
		switch pgErr.Code {
		case "23505":
			return ErrDuplicate
		case "23503":
			return ErrBadReference
		case "P0001":
			switch pgErr.Message {
			case "accounting_period_locked":
				return ErrPeriodLocked
			case "posted_journal_immutable", "posted_journal_lines_immutable":
				return ErrJournalImmutable
			case "journal_entry_missing":
				return ErrNotFound
			default:
				return ErrJournalIntegrity
			}
		}
	}
	return err
}

// --- pgtype helpers ----------------------------------------------------------

func textOrNil(v string) pgtype.Text {
	return pgtype.Text{String: v, Valid: v != ""}
}

func dateOrNil(v string) pgtype.Date {
	var d pgtype.Date
	if t, err := parseDate(v); err == nil {
		d = pgtype.Date{Time: t, Valid: true}
	}
	return d
}

func int8OrNil(v int64) pgtype.Int8 {
	return pgtype.Int8{Int64: v, Valid: v > 0}
}

func boolOrNil(v *bool) pgtype.Bool {
	if v == nil {
		return pgtype.Bool{}
	}
	return pgtype.Bool{Bool: *v, Valid: true}
}

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

func (r *Repository) ListAccounts(ctx context.Context, active *bool, search string) ([]db.Account, error) {
	rows, err := r.q.AcctListAccounts(ctx, db.AcctListAccountsParams{Active: boolOrNil(active), Search: textOrNil(search)})
	return rows, mapErr(err)
}

func (r *Repository) GetAccountByCode(ctx context.Context, q db.DBTX, code string) (db.Account, error) {
	row, err := db.New(q).AcctGetAccountByCode(ctx, code)
	return row, mapErr(err)
}

func (r *Repository) CreateAccount(ctx context.Context, p db.AcctCreateAccountParams) (db.Account, error) {
	row, err := r.q.AcctCreateAccount(ctx, p)
	return row, mapErr(err)
}

func (r *Repository) UpdateAccount(ctx context.Context, p db.AcctUpdateAccountParams) (db.Account, error) {
	row, err := r.q.AcctUpdateAccount(ctx, p)
	return row, mapErr(err)
}

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

func (r *Repository) ListDepartments(ctx context.Context, active *bool, search string) ([]db.AccountingDepartment, error) {
	rows, err := r.q.AcctListDepartments(ctx, db.AcctListDepartmentsParams{Active: boolOrNil(active), Search: textOrNil(search)})
	return rows, mapErr(err)
}

func (r *Repository) GetDepartmentByCode(ctx context.Context, q db.DBTX, code string) (db.AccountingDepartment, error) {
	row, err := db.New(q).AcctGetDepartmentByCode(ctx, code)
	return row, mapErr(err)
}

func (r *Repository) CreateDepartment(ctx context.Context, p db.AcctCreateDepartmentParams) (db.AccountingDepartment, error) {
	row, err := r.q.AcctCreateDepartment(ctx, p)
	return row, mapErr(err)
}

func (r *Repository) UpdateDepartment(ctx context.Context, p db.AcctUpdateDepartmentParams) (db.AccountingDepartment, error) {
	row, err := r.q.AcctUpdateDepartment(ctx, p)
	return row, mapErr(err)
}

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

func (r *Repository) journalParams(f JournalFilter) (db.AcctCountJournalsParams, db.AcctListJournalsParams) {
	count := db.AcctCountJournalsParams{
		JournalType:         textOrNil(f.JournalType),
		AllowedJournalTypes: f.AllowedJournalTypes,
		Status:              textOrNil(f.Status),
		StoreID:             int8OrNil(f.StoreID),
		AllowedStoreIds:     f.AllowedStoreIDs,
		DateFrom:            dateOrNil(f.DateFrom),
		DateTo:              dateOrNil(f.DateTo),
		Search:              textOrNil(f.Search),
	}
	list := db.AcctListJournalsParams{
		Limit:               int32(f.PerPage),
		Offset:              int32((f.Page - 1) * f.PerPage),
		JournalType:         count.JournalType,
		AllowedJournalTypes: count.AllowedJournalTypes,
		Status:              count.Status,
		StoreID:             count.StoreID,
		AllowedStoreIds:     count.AllowedStoreIds,
		DateFrom:            count.DateFrom,
		DateTo:              count.DateTo,
		Search:              count.Search,
	}
	return count, list
}

func (r *Repository) CountJournals(ctx context.Context, f JournalFilter) (int, error) {
	c, l := r.journalParams(f)
	_ = l
	total, err := r.q.AcctCountJournals(ctx, c)
	return int(total), mapErr(err)
}

func (r *Repository) ListJournals(ctx context.Context, f JournalFilter) ([]db.JournalEntry, error) {
	_, p := r.journalParams(f)
	rows, err := r.q.AcctListJournals(ctx, p)
	return rows, mapErr(err)
}

func (r *Repository) ListJournalLinesByEntries(ctx context.Context, entryIDs []int64) ([]db.AcctListJournalLinesByEntriesRow, error) {
	if len(entryIDs) == 0 {
		return nil, nil
	}
	rows, err := r.q.AcctListJournalLinesByEntries(ctx, entryIDs)
	return rows, mapErr(err)
}

func (r *Repository) GetJournal(ctx context.Context, id int64) (db.JournalEntry, error) {
	row, err := r.q.AcctGetJournal(ctx, id)
	return row, mapErr(err)
}

// JournalBySource drills one upstream document to its auto-drafted journal
// (GR accrual 'goods_receipt'/GR-id, advances). Latest wins.
func (r *Repository) JournalBySource(ctx context.Context, sourceType string, sourceID int64) (db.JournalEntry, error) {
	row, err := r.q.AcctJournalBySource(ctx, db.AcctJournalBySourceParams{
		SourceType: pgtype.Text{String: sourceType, Valid: sourceType != ""},
		SourceID:   pgtype.Int8{Int64: sourceID, Valid: true},
	})
	return row, mapErr(err)
}

func (r *Repository) LockJournal(ctx context.Context, q db.DBTX, id int64) (db.JournalEntry, error) {
	row, err := db.New(q).AcctGetJournalForUpdate(ctx, id)
	return row, mapErr(err)
}

func (r *Repository) CreateJournal(ctx context.Context, q db.DBTX, p db.AcctCreateJournalParams) (db.JournalEntry, error) {
	row, err := db.New(q).AcctCreateJournal(ctx, p)
	return row, mapErr(err)
}

func (r *Repository) UpdateJournal(ctx context.Context, q db.DBTX, p db.AcctUpdateJournalParams) error {
	_, err := db.New(q).AcctUpdateJournal(ctx, p)
	return mapErr(err)
}

func (r *Repository) SetJournalPosted(ctx context.Context, q db.DBTX, p db.AcctSetJournalPostedParams) error {
	_, err := db.New(q).AcctSetJournalPosted(ctx, p)
	return mapErr(err)
}

func (r *Repository) SetJournalVoided(ctx context.Context, q db.DBTX, id int64) error {
	_, err := db.New(q).AcctSetJournalVoided(ctx, id)
	return mapErr(err)
}

func (r *Repository) SetJournalReversed(ctx context.Context, q db.DBTX, p db.AcctSetJournalReversedParams) error {
	_, err := db.New(q).AcctSetJournalReversed(ctx, p)
	return mapErr(err)
}

func (r *Repository) DeleteJournal(ctx context.Context, q db.DBTX, id int64) error {
	_, err := db.New(q).AcctDeleteJournal(ctx, id)
	return mapErr(err)
}

func (r *Repository) CreateJournalLine(ctx context.Context, q db.DBTX, p db.AcctCreateJournalLineParams) (int64, error) {
	id, err := db.New(q).AcctCreateJournalLine(ctx, p)
	return id, mapErr(err)
}

func (r *Repository) DeleteJournalLines(ctx context.Context, q db.DBTX, entryID int64) error {
	return mapErr(db.New(q).AcctDeleteJournalLines(ctx, entryID))
}

// --- ledger / trial balance ------------------------------------------------------

func (r *Repository) ledgerParams(f LedgerFilter) db.AcctLedgerLinesParams {
	return db.AcctLedgerLinesParams{
		Status:          textOrNil(f.Status),
		JournalType:     textOrNil(f.JournalType),
		StoreID:         int8OrNil(f.StoreID),
		AllowedStoreIds: f.AllowedStoreIDs,
		DateFrom:        dateOrNil(f.DateFrom),
		DateTo:          dateOrNil(f.DateTo),
		AccountCode:     textOrNil(f.AccountCode),
		AccountType:     textOrNil(f.AccountType),
		Search:          textOrNil(f.Search),
	}
}

func (r *Repository) LedgerLines(ctx context.Context, f LedgerFilter) ([]db.AcctLedgerLinesRow, error) {
	rows, err := r.q.AcctLedgerLines(ctx, r.ledgerParams(f))
	return rows, mapErr(err)
}

func (r *Repository) LedgerOpening(ctx context.Context, f LedgerFilter) ([]db.AcctLedgerOpeningRow, error) {
	p := r.ledgerParams(f)
	rows, err := r.q.AcctLedgerOpening(ctx, db.AcctLedgerOpeningParams{
		Status:          p.Status,
		DateFrom:        p.DateFrom,
		JournalType:     p.JournalType,
		StoreID:         p.StoreID,
		AllowedStoreIds: p.AllowedStoreIds,
		AccountCode:     p.AccountCode,
		AccountType:     p.AccountType,
		Search:          p.Search,
	})
	return rows, mapErr(err)
}

func (r *Repository) TrialBalanceRows(ctx context.Context, dateFrom, dateTo string) ([]db.AcctTrialBalanceRow, error) {
	rows, err := r.q.AcctTrialBalance(ctx, db.AcctTrialBalanceParams{DateFrom: dateOrNil(dateFrom), DateTo: dateOrNil(dateTo)})
	return rows, mapErr(err)
}

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

func (r *Repository) advanceParams(f AdvanceFilter) (db.AcctCountAdvancesParams, db.AcctListAdvancesParams) {
	count := db.AcctCountAdvancesParams{
		Type:   textOrNil(f.Type),
		Status: textOrNil(f.Status),
		Search: textOrNil(f.Search),
	}
	list := db.AcctListAdvancesParams{
		Limit:  int32(f.PerPage),
		Offset: int32((f.Page - 1) * f.PerPage),
		Type:   count.Type,
		Status: count.Status,
		Search: count.Search,
	}
	return count, list
}

func (r *Repository) CountAdvances(ctx context.Context, f AdvanceFilter) (int, error) {
	c, _ := r.advanceParams(f)
	total, err := r.q.AcctCountAdvances(ctx, c)
	return int(total), mapErr(err)
}

func (r *Repository) ListAdvances(ctx context.Context, f AdvanceFilter) ([]db.AcctListAdvancesRow, error) {
	_, p := r.advanceParams(f)
	rows, err := r.q.AcctListAdvances(ctx, p)
	return rows, mapErr(err)
}

func (r *Repository) GetAdvance(ctx context.Context, id int64) (db.AcctGetAdvanceRow, error) {
	row, err := r.q.AcctGetAdvance(ctx, id)
	return row, mapErr(err)
}

func (r *Repository) LockAdvance(ctx context.Context, q db.DBTX, id int64) (db.Advance, error) {
	row, err := db.New(q).AcctGetAdvanceForUpdate(ctx, id)
	return row, mapErr(err)
}

func (r *Repository) CreateAdvance(ctx context.Context, q db.DBTX, p db.AcctCreateAdvanceParams) (db.Advance, error) {
	row, err := db.New(q).AcctCreateAdvance(ctx, p)
	return row, mapErr(err)
}

func (r *Repository) SetAdvanceAmounts(ctx context.Context, q db.DBTX, p db.AcctSetAdvanceAmountsParams) error {
	_, err := db.New(q).AcctSetAdvanceAmounts(ctx, p)
	return mapErr(err)
}

func (r *Repository) CreateAdvanceTxn(ctx context.Context, q db.DBTX, p db.AcctCreateAdvanceTxnParams) (db.AdvanceTransaction, error) {
	row, err := db.New(q).AcctCreateAdvanceTxn(ctx, p)
	return row, mapErr(err)
}

func (r *Repository) ListAdvanceTxns(ctx context.Context, advanceID int64) ([]db.AdvanceTransaction, error) {
	rows, err := r.q.AcctListAdvanceTxns(ctx, advanceID)
	return rows, mapErr(err)
}
