package controllers

import (
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"

	"nearle/models"
	"nearle/services"

	"github.com/gofiber/fiber/v2"
)

// HTTP face of the POS terminal ingest.
//
// These handlers break this codebase's house style in one respect, on purpose:
// they answer with a bare ack rather than the usual
// `{code, message, status, details}` envelope. The terminal reads `accepted`
// from the top level of the body and marks a bill synced only if its id is
// there — wrapping the ack would leave every till queueing for ever.
//
// The status code carries the other half of the contract:
//
//   - **200** — the batch was processed. Individual bills may still have been
//     refused; the ack says which.
//   - **4xx** — the request itself is wrong (unreadable body, unknown outlet).
//     The terminal treats these as non-retryable and halts, so a person is
//     told rather than the broker hammered.
//   - **5xx** — the outcome is unknown. The terminal keeps every bill and
//     retries with backoff. This is the right answer when the database is
//     having a bad minute: *never* ack a batch that did not commit.
type PosController struct {
	posService services.PosService
}

func NewPosController(posService services.PosService) *PosController {
	return &PosController{posService: posService}
}

// IngestOrders receives a batch of completed counter bills.
func (ctl *PosController) IngestOrders(c *fiber.Ctx) error {
	var batch models.PosOrderBatch

	if err := c.BodyParser(&batch); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{
			"code":    http.StatusBadRequest,
			"message": "could not read the batch: " + err.Error(),
			"status":  false,
		})
	}

	if strings.TrimSpace(batch.Storeid) == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{
			"code":    http.StatusBadRequest,
			"message": "store_id is required",
			"status":  false,
		})
	}

	ack, err := ctl.posService.IngestOrders(batch)
	if err != nil {
		return posIngestError(c, "IngestOrders", err)
	}

	return c.Status(http.StatusOK).JSON(ack)
}

// IngestCustomers receives shoppers registered at a till.
func (ctl *PosController) IngestCustomers(c *fiber.Ctx) error {
	var batch models.PosCustomerBatch

	if err := c.BodyParser(&batch); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{
			"code":    http.StatusBadRequest,
			"message": "could not read the batch: " + err.Error(),
			"status":  false,
		})
	}

	if strings.TrimSpace(batch.Storeid) == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{
			"code":    http.StatusBadRequest,
			"message": "store_id is required",
			"status":  false,
		})
	}

	ack, err := ctl.posService.IngestCustomers(batch)
	if err != nil {
		return posIngestError(c, "IngestCustomers", err)
	}

	return c.Status(http.StatusOK).JSON(ack)
}

// Catalogue answers a terminal's product pull.
func (ctl *PosController) Catalogue(c *fiber.Ctx) error {
	storeID := strings.TrimSpace(c.Query("store_id"))
	if storeID == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{
			"code":    http.StatusBadRequest,
			"message": "store_id is required",
			"status":  false,
		})
	}

	page, _ := strconv.Atoi(c.Query("page", "0"))
	pageSize, _ := strconv.Atoi(c.Query("page_size", "500"))

	result, err := ctl.posService.Catalogue(storeID, c.Query("since"), page, pageSize)
	if err != nil {
		return posIngestError(c, "Catalogue", err)
	}

	return c.Status(http.StatusOK).JSON(result)
}

// TerminalHealth returns one till's live state, for a support call that starts
// with a terminal code.
func (ctl *PosController) TerminalHealth(c *fiber.Ctx) error {
	terminalID := strings.TrimSpace(c.Query("terminal_id"))
	if terminalID == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{
			"code": http.StatusBadRequest, "message": "terminal_id is required", "status": false,
		})
	}

	fields, err := ctl.posService.TerminalHealth(c.Context(), terminalID)
	if err != nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{
			"code": http.StatusServiceUnavailable, "message": err.Error(), "status": false,
		})
	}

	if fields == nil {
		// Not an error. The till has simply not reported inside its TTL, which
		// is the answer the caller wanted — said plainly rather than as a 404
		// that reads like the terminal does not exist.
		return c.JSON(fiber.Map{
			"code":   http.StatusOK,
			"status": true,
			"details": fiber.Map{
				"terminal_id": terminalID,
				"status":      "offline",
				"reason":      "no heartbeat received within the presence window",
			},
		})
	}

	return c.JSON(fiber.Map{"code": http.StatusOK, "status": true, "details": fields})
}

// LocationHealth returns every till at a shop — the "which counters are dark"
// board. Tills that have stopped reporting come back marked offline rather than
// being omitted, because a missing till is exactly what somebody is looking for.
func (ctl *PosController) LocationHealth(c *fiber.Ctx) error {
	locationID := strings.TrimSpace(c.Query("location_id"))
	if locationID == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{
			"code": http.StatusBadRequest, "message": "location_id is required", "status": false,
		})
	}

	terminals, err := ctl.posService.LocationHealth(c.Context(), locationID)
	if err != nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{
			"code": http.StatusServiceUnavailable, "message": err.Error(), "status": false,
		})
	}

	online := 0
	for _, t := range terminals {
		if t["status"] == "online" {
			online++
		}
	}

	return c.JSON(fiber.Map{
		"code":   http.StatusOK,
		"status": true,
		"details": fiber.Map{
			"location_id": locationID,
			"total":       len(terminals),
			"online":      online,
			"terminals":   terminals,
		},
	})
}

// ---------------------------------------------------------------- Sales reads
//
// Unlike the ingest handlers above, these answer in the usual
// `{code, message, status, details}` envelope — they are read by the web app,
// not by a terminal, and nothing about them is bound to the till's contract.

// posSalesFilter reads the shared query parameters.
func posSalesFilter(c *fiber.Ctx) (models.PosSalesFilter, error) {
	locationID, err := strconv.Atoi(strings.TrimSpace(c.Query("locationid")))
	if err != nil || locationID <= 0 {
		return models.PosSalesFilter{}, fmt.Errorf("locationid is required")
	}

	pageno, _ := strconv.Atoi(c.Query("pageno", "0"))
	pagesize, _ := strconv.Atoi(c.Query("pagesize", "50"))

	return models.PosSalesFilter{
		Locationid:  locationID,
		Fromdate:    strings.TrimSpace(c.Query("fromdate")),
		Todate:      strings.TrimSpace(c.Query("todate")),
		Terminalid:  strings.TrimSpace(c.Query("terminalid")),
		Cashiername: strings.TrimSpace(c.Query("cashiername")),
		Paymentmode: strings.TrimSpace(c.Query("paymentmode")),
		Pageno:      pageno,
		Pagesize:    pagesize,
	}, nil
}

// GetSales lists counter bills for an outlet, newest first.
func (ctl *PosController) GetSales(c *fiber.Ctx) error {
	filter, err := posSalesFilter(c)
	if err != nil {
		return posBadRequest(c, err)
	}

	page, err := ctl.posService.Sales(filter)
	if err != nil {
		return posServerError(c, "GetSales", err)
	}

	return c.JSON(fiber.Map{"code": http.StatusOK, "status": true, "details": page})
}

// GetSaleDetail returns one bill with its lines.
//
// Accepts the terminal's order UUID, the invoice number, or this backend's
// posorderid — a support call starts from whichever the caller is looking at.
func (ctl *PosController) GetSaleDetail(c *fiber.Ctx) error {
	locationID, err := strconv.Atoi(strings.TrimSpace(c.Query("locationid")))
	if err != nil || locationID <= 0 {
		return posBadRequest(c, fmt.Errorf("locationid is required"))
	}

	reference := strings.TrimSpace(c.Query("reference"))
	if reference == "" {
		return posBadRequest(c, fmt.Errorf("reference is required — an order id, invoice number or posorderid"))
	}

	bill, err := ctl.posService.SaleDetail(locationID, reference)
	if err != nil {
		return posServerError(c, "GetSaleDetail", err)
	}
	if bill == nil {
		return c.Status(http.StatusNotFound).JSON(fiber.Map{
			"code":    http.StatusNotFound,
			"message": "no bill matches that reference at this outlet",
			"status":  false,
		})
	}

	return c.JSON(fiber.Map{"code": http.StatusOK, "status": true, "details": bill})
}

// GetSalesSummary totals a range, split by tender, day and till.
func (ctl *PosController) GetSalesSummary(c *fiber.Ctx) error {
	filter, err := posSalesFilter(c)
	if err != nil {
		return posBadRequest(c, err)
	}

	summary, err := ctl.posService.SalesSummary(filter)
	if err != nil {
		return posServerError(c, "GetSalesSummary", err)
	}

	return c.JSON(fiber.Map{"code": http.StatusOK, "status": true, "details": summary})
}

func posBadRequest(c *fiber.Ctx, err error) error {
	return c.Status(http.StatusBadRequest).JSON(fiber.Map{
		"code": http.StatusBadRequest, "message": err.Error(), "status": false,
	})
}

func posServerError(c *fiber.Ctx, op string, err error) error {
	log.Printf("pos %s: %v", op, err)
	return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
		"code": http.StatusInternalServerError, "message": err.Error(), "status": false,
	})
}

// posIngestError decides whether the terminal should retry.
//
// The distinction matters more than the message does. A misconfigured store id
// will be just as wrong on the next attempt, so it is reported as a 4xx and the
// till halts and shows a person the reason. Anything else might succeed later,
// so it is a 5xx and the bills stay queued.
func posIngestError(c *fiber.Ctx, op string, err error) error {
	log.Printf("pos %s: %v", op, err)

	message := err.Error()
	lower := strings.ToLower(message)

	permanent := strings.Contains(lower, "is not a location id") ||
		strings.Contains(lower, "no outlet is registered") ||
		strings.Contains(lower, "does not belong to tenant") ||
		strings.Contains(lower, "has no products stocked") ||
		strings.Contains(lower, "no applocationid configured")

	status := http.StatusInternalServerError
	if permanent {
		status = http.StatusBadRequest
	}

	return c.Status(status).JSON(fiber.Map{
		"code":    status,
		"message": message,
		"status":  false,
	})
}
