package cashier

import (
	"context"
	"errors"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/shopspring/decimal"

	"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)
}

// queries binds the sqlc queries to the caller's transaction, falling back to
// the pool only when the caller is not inside one.
func (r *Repository) queries(q db.DBTX) *db.Queries {
	if q == nil {
		return r.q
	}
	return db.New(q)
}

func (r *Repository) CreateShift(ctx context.Context, q db.DBTX, p db.CreateCashierShiftParams) (db.CashierShift, error) {
	return r.queries(q).CreateCashierShift(ctx, p)
}

func (r *Repository) SetShiftBinding(ctx context.Context, q db.DBTX, p db.SetShiftBindingParams) (db.CashierShift, error) {
	return r.queries(q).SetShiftBinding(ctx, p)
}

func (r *Repository) GetShiftByID(ctx context.Context, q db.DBTX, id int64) (db.CashierShift, error) {
	row, err := r.queries(q).GetCashierShiftByID(ctx, id)
	if errors.Is(err, pgx.ErrNoRows) {
		return db.CashierShift{}, ErrNotFound
	}
	return row, err
}

func (r *Repository) GetShiftByIDForUpdate(ctx context.Context, q db.DBTX, id int64) (db.CashierShift, error) {
	row, err := r.queries(q).GetCashierShiftByIDForUpdate(ctx, id)
	if errors.Is(err, pgx.ErrNoRows) {
		return db.CashierShift{}, ErrNotFound
	}
	return row, err
}

func (r *Repository) GetOpenShiftByCashier(ctx context.Context, q db.DBTX, cashierID int64) (db.CashierShift, error) {
	row, err := r.queries(q).GetOpenShiftByCashier(ctx, cashierID)
	if errors.Is(err, pgx.ErrNoRows) {
		return db.CashierShift{}, ErrNotFound
	}
	return row, err
}

func (r *Repository) GetOpenShiftByCashierForUpdate(ctx context.Context, q db.DBTX, cashierID int64) (db.CashierShift, error) {
	var res db.CashierShift
	err := q.QueryRow(ctx, `SELECT id, shift_number, cashier_id, status, opened_at, opening_cash, closed_at, actual_cash, variance, notes, register_id, store_id, created_at, updated_at FROM cashier_shifts WHERE cashier_id=$1 AND status='open' LIMIT 1 FOR UPDATE`, cashierID).Scan(
		&res.ID, &res.ShiftNumber, &res.CashierID, &res.Status, &res.OpenedAt, &res.OpeningCash, &res.ClosedAt, &res.ActualCash, &res.Variance, &res.Notes, &res.RegisterID, &res.StoreID, &res.CreatedAt, &res.UpdatedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return db.CashierShift{}, ErrNotFound
	}
	return res, err
}

func (r *Repository) CloseShift(ctx context.Context, q db.DBTX, id int64, actualCash, variance decimal.Decimal, notes *string) error {
	execer := q
	if execer == nil {
		execer = r.pool
	}
	var noteVal *string
	if notes != nil && *notes != "" {
		noteVal = notes
	}
	tag, err := execer.Exec(ctx, `
		UPDATE cashier_shifts
		SET status='closed', closed_at=now(), actual_cash=$2, variance=$3,
		    notes = CASE 
		        WHEN $4::text IS NOT NULL AND $4::text <> '' THEN 
		            CASE WHEN notes IS NOT NULL AND notes <> '' THEN notes || E'\n' || $4::text ELSE $4::text END
		        ELSE notes 
		    END,
		    updated_at=now()
		WHERE id=$1`, id, actualCash, variance, noteVal)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) ListShifts(ctx context.Context, q db.DBTX, f db.ListCashierShiftsParams) ([]db.ListCashierShiftsRow, error) {
	return r.queries(q).ListCashierShifts(ctx, f)
}

func (r *Repository) CountShifts(ctx context.Context, q db.DBTX, f db.CountCashierShiftsParams) (int64, error) {
	return r.queries(q).CountCashierShifts(ctx, f)
}

func (r *Repository) CreateDrawerEntry(ctx context.Context, q db.DBTX, p db.CreateDrawerEntryParams) (db.CashierDrawerEntry, error) {
	return r.queries(q).CreateDrawerEntry(ctx, p)
}

func (r *Repository) ListDrawerEntries(ctx context.Context, q db.DBTX, shiftID int64) ([]db.CashierDrawerEntry, error) {
	return r.queries(q).ListDrawerEntries(ctx, shiftID)
}

func (r *Repository) SumDrawerByShift(ctx context.Context, q db.DBTX, shiftID int64) ([]db.SumDrawerByShiftRow, error) {
	return r.queries(q).SumDrawerByShift(ctx, shiftID)
}

func (r *Repository) SumDrawerByShifts(ctx context.Context, q db.DBTX, shiftIDs []int64) ([]db.SumDrawerByShiftsRow, error) {
	if len(shiftIDs) == 0 {
		return nil, nil
	}
	return r.queries(q).SumDrawerByShifts(ctx, shiftIDs)
}

func (r *Repository) SumCashSalesByShift(ctx context.Context, q db.DBTX, shiftID int64) (decimal.Decimal, error) {
	return r.queries(q).SumCashSalesByShift(ctx, pgtype.Int8{Int64: shiftID, Valid: true})
}

func (r *Repository) SumNonCashSalesByShift(ctx context.Context, q db.DBTX, shiftID int64) (decimal.Decimal, error) {
	return r.queries(q).SumNonCashSalesByShift(ctx, pgtype.Int8{Int64: shiftID, Valid: true})
}

func (r *Repository) CountOrdersByShift(ctx context.Context, q db.DBTX, shiftID int64) (int64, error) {
	return r.queries(q).CountOrdersByShift(ctx, pgtype.Int8{Int64: shiftID, Valid: true})
}

func (r *Repository) SetSalesOrderShift(ctx context.Context, q db.DBTX, orderID, shiftID int64) error {
	n, err := r.queries(q).SetSalesOrderShift(ctx, db.SetSalesOrderShiftParams{ID: orderID, ShiftID: pgtype.Int8{Int64: shiftID, Valid: true}})
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) RecordClosureAudit(ctx context.Context, q db.DBTX, id int64, a ClosureAudit) error {
	_, err := q.Exec(ctx, `INSERT INTO cashier_shift_closures (shift_id, expected_cash, actual_cash, difference, actor_id, reason, override) VALUES ($1,$2,$3,$4,$5,$6,$7)`, id, a.ExpectedCash, a.ActualCash, a.Difference, a.ActorID, a.Reason, a.Override)
	return err
}

func (r *Repository) ListClosureAudit(ctx context.Context, q db.DBTX, id int64) ([]ClosureAudit, error) {
	rows, err := q.Query(ctx, `SELECT expected_cash, actual_cash, difference, actor_id, reason, override, created_at FROM cashier_shift_closures WHERE shift_id=$1 ORDER BY id`, id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []ClosureAudit{}
	for rows.Next() {
		var a ClosureAudit
		if err := rows.Scan(&a.ExpectedCash, &a.ActualCash, &a.Difference, &a.ActorID, &a.Reason, &a.Override, &a.CreatedAt); err != nil {
			return nil, err
		}
		out = append(out, a)
	}
	return out, rows.Err()
}

func (r *Repository) IsStoreActive(ctx context.Context, q db.DBTX, storeID int64) (bool, error) {
	return r.queries(q).IsStoreActive(ctx, storeID)
}

func (r *Repository) IsUserInActiveStore(ctx context.Context, q db.DBTX, userID, storeID int64) (bool, error) {
	return r.queries(q).IsUserInActiveStore(ctx, db.IsUserInActiveStoreParams{
		UserID:  userID,
		StoreID: storeID,
	})
}

func (r *Repository) ActiveStoresForUser(ctx context.Context, q db.DBTX, userID int64) ([]int64, error) {
	return r.queries(q).StoresForUser(ctx, userID)
}
