package services

import (
	"context"
	"time"

	"nearle/models"
	"nearle/repositories"
	"nearle/utils"
)

type PosService interface {
	IngestOrders(batch models.PosOrderBatch) (*models.PosAck, error)
	IngestCustomers(batch models.PosCustomerBatch) (*models.PosAck, error)
	Catalogue(storeID, since string, page, pageSize int) (*models.PosCatalogueResponse, error)

	// RecordHealth stores one heartbeat. Never acknowledged back to the till:
	// presence is a fire-and-forget signal, and a terminal that stopped selling
	// because its heartbeat failed would be a worse outcome than a blank board.
	RecordHealth(ctx context.Context, health models.PosHealth) error

	TerminalHealth(ctx context.Context, terminalID string) (map[string]string, error)
	LocationHealth(ctx context.Context, locationID string) ([]map[string]string, error)

	Sales(f models.PosSalesFilter) (*models.PosSalesPage, error)
	SaleDetail(locationID int, reference string) (*models.PosOrders, error)
	SalesSummary(f models.PosSalesFilter) (*models.PosSalesSummary, error)

	// Login authenticates a person against the same account store the web
	// console uses and mints the session a till carries for the trading day.
	Login(req models.PosLoginRequest) (*models.PosSession, error)

	// LocationAllowed is the authorisation check every other POS call rests on:
	// does the tenant in the caller's token actually own this outlet.
	LocationAllowed(tenantID, locationID int) (bool, error)

	// Staff lists who may ring a bill at an outlet. Sent with the session and
	// available on its own, so a shop that hires someone mid-shift can pull them
	// down without signing the terminal out.
	Staff(tenantID, locationID int) ([]models.PosStaffMember, error)

	// Till staff management, all scoped to the caller's own outlet.
	CreateUser(tenantID, locationID, configID int, req models.PosUserRequest) (*models.PosUser, error)
	UpdateUser(tenantID, locationID int, req models.PosUserRequest) (*models.PosUser, error)
	ListUsers(tenantID, locationID int, includeInactive bool) ([]models.PosUser, error)
	DeactivateUser(tenantID, locationID, userID int) error

	// LoginWithPin signs a person in at a terminal that is already open. Never
	// reachable anonymously — four digits is not a barrier on its own.
	LoginWithPin(tenantID, locationID int, pin string) (*models.PosSession, error)

	// ConfigidFor infers which portal a tenant's people belong to, so the console
	// is never asked for a number nobody can look up.
	ConfigidFor(tenantID int) int
}

type posService struct {
	repo     repositories.PosRepository
	presence repositories.PosPresenceRepository
}

func NewPosService(repo repositories.PosRepository, presence repositories.PosPresenceRepository) PosService {
	return &posService{repo: repo, presence: presence}
}

func (s *posService) RecordHealth(ctx context.Context, health models.PosHealth) error {
	return s.presence.Record(ctx, health)
}

func (s *posService) TerminalHealth(ctx context.Context, terminalID string) (map[string]string, error) {
	return s.presence.Terminal(ctx, terminalID)
}

func (s *posService) LocationHealth(ctx context.Context, locationID string) ([]map[string]string, error) {
	return s.presence.Location(ctx, locationID)
}

func (s *posService) IngestOrders(batch models.PosOrderBatch) (*models.PosAck, error) {
	return s.repo.IngestOrders(batch)
}

func (s *posService) IngestCustomers(batch models.PosCustomerBatch) (*models.PosAck, error) {
	return s.repo.IngestCustomers(batch)
}

func (s *posService) Catalogue(storeID, since string, page, pageSize int) (*models.PosCatalogueResponse, error) {
	return s.repo.Catalogue(storeID, since, page, pageSize)
}

func (s *posService) Sales(f models.PosSalesFilter) (*models.PosSalesPage, error) {
	return s.repo.Sales(f)
}

func (s *posService) SaleDetail(locationID int, reference string) (*models.PosOrders, error) {
	return s.repo.SaleDetail(locationID, reference)
}

func (s *posService) SalesSummary(f models.PosSalesFilter) (*models.PosSalesSummary, error) {
	return s.repo.SalesSummary(f)
}

// Login authenticates a terminal's operator and issues its session.
//
// The token is minted here rather than in the repository so that the signing
// key stays out of the layer that talks to the database, and so a future change
// of token format touches one function.
func (s *posService) Login(req models.PosLoginRequest) (*models.PosSession, error) {
	session, err := s.repo.PosLogin(req)
	if err != nil {
		return nil, err
	}

	return s.mint(session, req.Terminalid)
}

// mint signs a resolved session.
//
// Kept apart from the credential checks so the signing key stays out of the
// layer that talks to the database, and so a change of token format touches one
// function rather than every way in.
func (s *posService) mint(session *models.PosSession, terminalID string) (*models.PosSession, error) {
	token, expires, err := utils.MintPosToken(utils.PosClaims{
		Userid:     session.Userid,
		Tenantid:   session.Tenantid,
		Locationid: session.Locationid,
		Roleid:     session.Roleid,
		Configid:   session.Configid,
		Terminalid: terminalID,
	}, time.Now())
	if err != nil {
		return nil, err
	}

	session.Token = token
	session.Expiresat = expires.UTC().Format(time.RFC3339)

	return session, nil
}

func (s *posService) LocationAllowed(tenantID, locationID int) (bool, error) {
	return s.repo.PosLocationAllowed(tenantID, locationID)
}

func (s *posService) Staff(tenantID, locationID int) ([]models.PosStaffMember, error) {
	return s.repo.PosStaff(tenantID, locationID)
}

func (s *posService) CreateUser(tenantID, locationID, configID int, req models.PosUserRequest) (*models.PosUser, error) {
	return s.repo.CreatePosUser(tenantID, locationID, configID, req)
}

func (s *posService) UpdateUser(tenantID, locationID int, req models.PosUserRequest) (*models.PosUser, error) {
	return s.repo.UpdatePosUser(tenantID, locationID, req)
}

func (s *posService) ListUsers(tenantID, locationID int, includeInactive bool) ([]models.PosUser, error) {
	return s.repo.ListPosUsers(tenantID, locationID, includeInactive)
}

func (s *posService) DeactivateUser(tenantID, locationID, userID int) error {
	return s.repo.DeactivatePosUser(tenantID, locationID, userID)
}

// LoginWithPin mints a fresh session for whoever the PIN belongs to.
//
// A new token rather than a reused one, because the token carries the role and
// a cashier taking over from a supervisor must not inherit their permissions.
func (s *posService) LoginWithPin(tenantID, locationID int, pin string) (*models.PosSession, error) {
	session, err := s.repo.PosLoginByPin(tenantID, locationID, pin)
	if err != nil {
		return nil, err
	}
	return s.mint(session, "")
}

func (s *posService) ConfigidFor(tenantID int) int {
	return s.repo.PosConfigidFor(tenantID)
}
