package repositories

import (
	"fmt"
	"strings"

	"nearle/models"
)

// Reading counter sales back out.
//
// The ingest side of this package only ever writes. Without these, a bill that
// reached pos_orders was invisible to every screen in the product — the data
// was safe and unreachable, which is its own kind of lost.
//
// Every query is scoped to one locationid. That is the authorisation boundary:
// a caller who omits it gets an error rather than a page through somebody
// else's takings.

// posSalesWhere builds the shared filter, so the list, the detail and the
// summary can never disagree about what "this outlet in this range" means.
func posSalesWhere(f models.PosSalesFilter) (string, []interface{}) {
	where := "locationid = ?"
	params := []interface{}{f.Locationid}

	// Matched on businessdate — the day the sale was rung, not the day it
	// reached us. A till that was offline overnight uploads yesterday's bills
	// this morning and they belong to yesterday.
	if f.Fromdate != "" && f.Todate != "" {
		where += " AND businessdate BETWEEN ? AND ?"
		params = append(params, f.Fromdate, f.Todate)
	} else if f.Fromdate != "" {
		where += " AND businessdate >= ?"
		params = append(params, f.Fromdate)
	} else if f.Todate != "" {
		where += " AND businessdate <= ?"
		params = append(params, f.Todate)
	}

	if t := strings.TrimSpace(f.Terminalid); t != "" {
		where += " AND terminalid = ?"
		params = append(params, t)
	}
	if c := strings.TrimSpace(f.Cashiername); c != "" {
		where += " AND cashiername = ?"
		params = append(params, c)
	}
	if p := strings.TrimSpace(f.Paymentmode); p != "" {
		where += " AND LOWER(paymentmode) = ?"
		params = append(params, strings.ToLower(p))
	}

	return where, params
}

// Sales returns a page of bills, newest first, with the total count.
//
// Line items are deliberately not included: a page of fifty bills would drag
// several hundred rows behind it, and a list screen shows none of them. Use
// SaleDetail for one bill.
func (r *posRepository) Sales(f models.PosSalesFilter) (*models.PosSalesPage, error) {
	if f.Locationid <= 0 {
		return nil, fmt.Errorf("locationid is required")
	}

	if f.Pagesize <= 0 || f.Pagesize > 500 {
		f.Pagesize = 50
	}
	if f.Pageno < 0 {
		f.Pageno = 0
	}

	where, params := posSalesWhere(f)

	var total int64
	if err := r.db.Raw(
		fmt.Sprintf(`SELECT COUNT(*) FROM pos_orders WHERE %s`, where),
		params...,
	).Scan(&total).Error; err != nil {
		return nil, err
	}

	bills := make([]models.PosOrders, 0)
	// Ordered by billedat rather than by id: a batch uploaded after an outage
	// arrives out of order, and a list sorted by arrival would interleave
	// yesterday's bills through today's.
	query := fmt.Sprintf(
		`SELECT * FROM pos_orders WHERE %s
		 ORDER BY billedat DESC, posorderid DESC
		 LIMIT ? OFFSET ?`, where)

	if err := r.db.Raw(query,
		append(params, f.Pagesize, f.Pageno*f.Pagesize)...,
	).Scan(&bills).Error; err != nil {
		return nil, err
	}

	return &models.PosSalesPage{
		Total:    total,
		Pageno:   f.Pageno,
		Pagesize: f.Pagesize,
		Bills:    bills,
	}, nil
}

// SaleDetail returns one bill with its lines.
//
// Accepts either the terminal's own order UUID or this backend's posorderid,
// because a support call starts from whichever the caller happens to be looking
// at — a receipt carries the invoice number, a log carries the UUID.
func (r *posRepository) SaleDetail(locationID int, reference string) (*models.PosOrders, error) {
	if locationID <= 0 {
		return nil, fmt.Errorf("locationid is required")
	}
	reference = strings.TrimSpace(reference)
	if reference == "" {
		return nil, fmt.Errorf("an order id, invoice number or posorderid is required")
	}

	var bill models.PosOrders
	err := r.db.Raw(`
		SELECT * FROM pos_orders
		WHERE locationid = ?
		  AND (terminalorderid = ? OR invoicenumber = ?
		       OR CAST(posorderid AS TEXT) = ?)
		LIMIT 1`,
		locationID, reference, reference, reference,
	).Scan(&bill).Error
	if err != nil {
		return nil, err
	}
	if bill.Posorderid == 0 {
		return nil, nil
	}

	items := make([]models.PosOrderItems, 0)
	if err := r.db.Raw(
		`SELECT * FROM pos_order_items WHERE posorderid = ? ORDER BY posorderitemid`,
		bill.Posorderid,
	).Scan(&items).Error; err != nil {
		return nil, err
	}
	bill.Items = items

	return &bill, nil
}

// SalesSummary totals a range, broken out the three ways somebody actually
// asks for: by tender, by day, and by till.
func (r *posRepository) SalesSummary(f models.PosSalesFilter) (*models.PosSalesSummary, error) {
	if f.Locationid <= 0 {
		return nil, fmt.Errorf("locationid is required")
	}

	where, params := posSalesWhere(f)

	summary := &models.PosSalesSummary{
		Locationid:    f.Locationid,
		Fromdate:      f.Fromdate,
		Todate:        f.Todate,
		Bypaymentmode: make([]models.PosPaymentTotal, 0),
		Byday:         make([]models.PosDayTotal, 0),
		Byterminal:    make([]models.PosTerminalTotal, 0),
	}

	var head struct {
		Billcount    int
		Itemcount    int
		Grosssales   float64
		Taxcollected float64
		Discount     float64
		Roundoff     float64
	}
	if err := r.db.Raw(fmt.Sprintf(`
		SELECT COUNT(*)                     AS billcount,
		       COALESCE(SUM(itemcount), 0)  AS itemcount,
		       COALESCE(SUM(total), 0)      AS grosssales,
		       COALESCE(SUM(taxamount), 0)  AS taxcollected,
		       COALESCE(SUM(discount), 0)   AS discount,
		       COALESCE(SUM(roundoff), 0)   AS roundoff
		FROM pos_orders WHERE %s`, where), params...).Scan(&head).Error; err != nil {
		return nil, err
	}

	summary.Billcount = head.Billcount
	summary.Itemcount = head.Itemcount
	summary.Grosssales = head.Grosssales
	summary.Taxcollected = head.Taxcollected
	summary.Discount = head.Discount
	summary.Roundoff = head.Roundoff
	if head.Billcount > 0 {
		summary.Averagebill = head.Grosssales / float64(head.Billcount)
	}

	if err := r.db.Raw(fmt.Sprintf(`
		SELECT COALESCE(paymentmode,'') AS paymentmode,
		       COUNT(*) AS billcount, COALESCE(SUM(total),0) AS amount
		FROM pos_orders WHERE %s
		GROUP BY paymentmode ORDER BY amount DESC`, where),
		params...).Scan(&summary.Bypaymentmode).Error; err != nil {
		return nil, err
	}

	if err := r.db.Raw(fmt.Sprintf(`
		SELECT businessdate, COUNT(*) AS billcount,
		       COALESCE(SUM(total),0) AS amount
		FROM pos_orders WHERE %s
		GROUP BY businessdate ORDER BY businessdate`, where),
		params...).Scan(&summary.Byday).Error; err != nil {
		return nil, err
	}

	if err := r.db.Raw(fmt.Sprintf(`
		SELECT COALESCE(terminalid,'') AS terminalid, COUNT(*) AS billcount,
		       COALESCE(SUM(total),0) AS amount
		FROM pos_orders WHERE %s
		GROUP BY terminalid ORDER BY amount DESC`, where),
		params...).Scan(&summary.Byterminal).Error; err != nil {
		return nil, err
	}

	return summary, nil
}
