package repositories_admin

import (
	"astrology-api/constants"
	usermodel "astrology-api/models/usermodel"
	"errors"
	"strings"
	"time"

	"gorm.io/gorm"
)

// The admin panel's reads over sessions that are still running, plus the two
// writes that close one from the panel.
//
// Like the settlement repository this owns its own filter type and its own
// flat read structs rather than depending on the transport DTOs, so the data
// layer does not have to change when a response shape does.

//////////////////////////////////////////////////////////////
// Filter and read structs
//////////////////////////////////////////////////////////////

type LiveFilter struct {
	Search string
	Status string
	Medium string

	AstrologerID uint
	UserID       uint

	StalledOnly bool
	FreeOnly    bool

	SortBy  string
	SortDir string

	Page  int
	Limit int

	// Computed by the service from the flags, so the repository does not have
	// to read configuration.
	StaleBefore time.Time
}

// LiveConsultationRow is a flat join, not a model.
type LiveConsultationRow struct {
	ConsultationID uint   `gorm:"column:id"`
	ConsultationNo string `gorm:"column:consultationNo"`

	UserID     uint   `gorm:"column:userId"`
	UserName   string `gorm:"column:userName"`
	UserMobile string `gorm:"column:userMobile"`
	UserEmail  string `gorm:"column:userEmail"`

	AstrologerID     uint   `gorm:"column:astrologerId"`
	AstrologerName   string `gorm:"column:astrologerName"`
	AstrologerMobile string `gorm:"column:astrologerMobile"`

	Medium    string `gorm:"column:medium"`
	Status    string `gorm:"column:status"`
	EndReason string `gorm:"column:endReason"`

	FreeMinutes int `gorm:"column:freeMinutes"`
	FreeSeconds int `gorm:"column:freeSeconds"`

	ChannelName   string  `gorm:"column:channelName"`
	RatePerMinute float64 `gorm:"column:ratePerMinute"`

	MaxBillableSeconds int `gorm:"column:maxBillableSeconds"`

	RequestedAt *time.Time `gorm:"column:requestedAt"`
	AcceptedAt  *time.Time `gorm:"column:acceptedAt"`
	StartedAt   *time.Time `gorm:"column:startedAt"`
	LastTickAt  *time.Time `gorm:"column:lastTickAt"`
	WarnedAt    *time.Time `gorm:"column:warnedAt"`
	CreatedAt   *time.Time `gorm:"column:created_at"`

	SettlementStatus string `gorm:"column:settlementStatus"`

	Name       string     `gorm:"column:name"`
	BirthDate  *time.Time `gorm:"column:birthDate"`
	BirthTime  string     `gorm:"column:birthTime"`
	BirthPlace string     `gorm:"column:birthPlace"`
	Gender     string     `gorm:"column:gender"`
}

// LiveAggregate is one GROUP BY row behind the dashboard header.
type LiveAggregate struct {
	Medium      string  `gorm:"column:medium"`
	Status      string  `gorm:"column:status"`
	FreeMinutes int     `gorm:"column:freeMinutes"`
	Total       int64   `gorm:"column:total"`
	Rate        float64 `gorm:"column:rate"`
}

type TranscriptRow struct {
	ID              uint       `gorm:"column:id"`
	ClientMessageID string     `gorm:"column:clientMessageId"`
	SenderType      string     `gorm:"column:senderType"`
	SenderID        uint       `gorm:"column:senderId"`
	MessageType     string     `gorm:"column:messageType"`
	Body            string     `gorm:"column:body"`
	SentAt          *time.Time `gorm:"column:sentAt"`
	Sequence        int        `gorm:"column:sequence"`
	UploadedBy      string     `gorm:"column:uploadedBy"`
}

//////////////////////////////////////////////////////////////
// Interface
//////////////////////////////////////////////////////////////

type ConsultationRepository interface {
	ListLive(filter LiveFilter) ([]LiveConsultationRow, int64, error)

	GetLiveRow(id uint) (*LiveConsultationRow, error)

	LiveAggregates() ([]LiveAggregate, error)

	GetConsultationsByIDs(ids []uint) ([]usermodel.Consultation, error)

	// CloseWithoutBilling ends a session that never ran. Re-checks the status
	// under a row lock, and reports whether this call is the one that moved
	// it, so a concurrent accept or sweep cannot be overwritten.
	CloseWithoutBilling(id uint, status string, endReason string, remarks string, adminID uint) (bool, error)

	StampAdminAction(id uint, remarks string, adminID uint) error

	AppendLog(consultationID uint, from string, to string, remarks string, adminID uint, source string) error

	ListMessages(consultationID uint, page int, limit int) ([]TranscriptRow, int64, error)

	CountMessages(consultationID uint) (int64, error)

	TranscriptUploaders(consultationID uint) ([]string, error)

	// FreeChatTotals is what the platform has spent on the free-chat offer:
	// how many have been claimed, and what this month's subsidy cost.
	FreeChatTotals() (int64, float64, error)
}

type consultationRepository struct {
	db *gorm.DB
}

func NewConsultationRepository(db *gorm.DB) ConsultationRepository {
	return &consultationRepository{db: db}
}

//////////////////////////////////////////////////////////////
// List
//////////////////////////////////////////////////////////////

func (r *consultationRepository) baseQuery() *gorm.DB {

	return r.db.
		Table("consultations AS c").
		Joins("LEFT JOIN users AS u ON u.id = c.userId").
		Joins("LEFT JOIN astrologers AS a ON a.id = c.astrologerId").
		Where("c.isDelete = ?", false)
}

func (r *consultationRepository) applyFilter(query *gorm.DB, filter LiveFilter) *gorm.DB {

	status := strings.ToUpper(strings.TrimSpace(filter.Status))

	switch status {

	case constants.ConsultationStatusRequested,
		constants.ConsultationStatusAccepted,
		constants.ConsultationStatusOngoing:

		query = query.Where("c.status = ?", status)

	default:

		query = query.Where("c.status IN ?", []string{
			constants.ConsultationStatusRequested,
			constants.ConsultationStatusAccepted,
			constants.ConsultationStatusOngoing,
		})
	}

	if medium := strings.ToUpper(strings.TrimSpace(filter.Medium)); medium != "" && medium != "ALL" {
		query = query.Where("c.medium = ?", medium)
	}

	if filter.AstrologerID > 0 {
		query = query.Where("c.astrologerId = ?", filter.AstrologerID)
	}

	if filter.UserID > 0 {
		query = query.Where("c.userId = ?", filter.UserID)
	}

	if filter.FreeOnly {
		query = query.Where("c.freeMinutes > ?", 0)
	}

	if filter.StalledOnly && !filter.StaleBefore.IsZero() {

		query = query.Where(
			"c.startedAt IS NOT NULL AND COALESCE(c.lastTickAt, c.startedAt, c.created_at) < ?",
			filter.StaleBefore,
		)
	}

	if search := strings.TrimSpace(filter.Search); search != "" {

		like := "%" + search + "%"

		query = query.Where(
			"c.consultationNo LIKE ? OR u.name LIKE ? OR u.contactNo LIKE ? OR a.name LIKE ?",
			like, like, like, like,
		)
	}

	return query
}

func (r *consultationRepository) ListLive(
	filter LiveFilter,
) ([]LiveConsultationRow, int64, error) {

	if filter.Page <= 0 {
		filter.Page = 1
	}

	if filter.Limit <= 0 {
		filter.Limit = 10
	}

	var total int64

	if err := r.applyFilter(r.baseQuery(), filter).Count(&total).Error; err != nil {
		return nil, 0, err
	}

	order := "c.created_at DESC"

	direction := "DESC"

	if strings.EqualFold(strings.TrimSpace(filter.SortDir), "asc") {
		direction = "ASC"
	}

	switch strings.TrimSpace(filter.SortBy) {

	case "startedAt":
		order = "c.startedAt " + direction

	case "requestedAt":
		order = "c.requestedAt " + direction

	case "rate":
		order = "c.ratePerMinute " + direction
	}

	var rows []LiveConsultationRow

	err := r.applyFilter(r.baseQuery(), filter).
		Select(liveColumns()).
		Order(order).
		Offset((filter.Page - 1) * filter.Limit).
		Limit(filter.Limit).
		Scan(&rows).Error

	return rows, total, err
}

func (r *consultationRepository) GetLiveRow(id uint) (*LiveConsultationRow, error) {

	var row LiveConsultationRow

	err := r.db.
		Table("consultations AS c").
		Joins("LEFT JOIN users AS u ON u.id = c.userId").
		Joins("LEFT JOIN astrologers AS a ON a.id = c.astrologerId").
		Where("c.id = ?", id).
		Select(liveColumns()).
		Take(&row).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, errors.New("consultation not found")
		}

		return nil, err
	}

	return &row, nil
}

func liveColumns() string {

	return `c.id, c.consultationNo, c.userId, c.astrologerId, c.medium, c.status,
	        c.endReason, c.freeMinutes, c.freeSeconds, c.channelName, c.ratePerMinute,
	        c.maxBillableSeconds, c.requestedAt, c.acceptedAt, c.startedAt,
	        c.lastTickAt, c.warnedAt, c.created_at, c.settlementStatus,
	        c.name, c.birthDate, c.birthTime, c.birthPlace, c.gender,
	        u.name AS userName, u.contactNo AS userMobile, u.email AS userEmail,
	        a.name AS astrologerName, a.contactNo AS astrologerMobile`
}

func (r *consultationRepository) LiveAggregates() ([]LiveAggregate, error) {

	var rows []LiveAggregate

	err := r.db.
		Table("consultations").
		Select("medium, status, freeMinutes, COUNT(*) AS total, COALESCE(SUM(ratePerMinute),0) AS rate").
		Where("isDelete = ?", false).
		Where("status IN ?", []string{
			constants.ConsultationStatusRequested,
			constants.ConsultationStatusAccepted,
			constants.ConsultationStatusOngoing,
		}).
		Group("medium, status, freeMinutes").
		Scan(&rows).Error

	return rows, err
}

func (r *consultationRepository) GetConsultationsByIDs(
	ids []uint,
) ([]usermodel.Consultation, error) {

	if len(ids) == 0 {
		return nil, nil
	}

	var records []usermodel.Consultation

	err := r.db.
		Where("id IN ?", ids).
		Where("isDelete = ?", false).
		Find(&records).Error

	return records, err
}

//////////////////////////////////////////////////////////////
// Writes
//////////////////////////////////////////////////////////////

func (r *consultationRepository) CloseWithoutBilling(
	id uint,
	status string,
	endReason string,
	remarks string,
	adminID uint,
) (bool, error) {

	now := time.Now()

	// The WHERE clause is the guard: a row a concurrent accept or sweep has
	// already moved no longer matches, and this call reports that it did
	// nothing rather than overwriting their decision.
	result := r.db.
		Table("consultations").
		Where("id = ?", id).
		Where("status IN ?", []string{
			constants.ConsultationStatusRequested,
			constants.ConsultationStatusAccepted,
			constants.ConsultationStatusOngoing,
		}).
		Updates(map[string]interface{}{
			"status":           status,
			"endReason":        endReason,
			"endedAt":          now,
			"durationSeconds":  0,
			"billedSeconds":    0,
			"billedMinutes":    0,
			"grossAmount":      0,
			"platformFeeAmount": 0,
			"astrologerEarning": 0,
			"settlementStatus": constants.SettlementStatusNA,

			// Hands the customer's free chat back: a session support closed
			// before it ran should not cost them their one freebie.
			"freeMinutes": 0,
			"freeSeconds": 0,

			"adminRemarks":  remarks,
			"adminActionBy": adminID,
			"adminActionAt": now,
			"updated_at":    now,
		})

	if result.Error != nil {
		return false, result.Error
	}

	if result.RowsAffected != 1 {
		return false, nil
	}

	// Take the astrologer off BUSY, as every customer/astrologer end path
	// does. Best effort: the session is already closed either way.
	_ = r.db.
		Table("astrologers").
		Where("id = (SELECT astrologerId FROM consultations WHERE id = ?)", id).
		Where("status = ?", "BUSY").
		Updates(map[string]interface{}{"status": "ONLINE", "is_available": 1}).Error

	// A waiting-queue row placed this consultation: a live one completes, one
	// still connecting is rejected. The next customer is handed the turn by
	// the queue pass of the sweep. Best effort, like the reset above.
	_ = r.db.
		Table("astrologer_waiting_queue").
		Where("consultation_id = ? AND status = ?", id, constants.QueueStatusConnected).
		Updates(map[string]interface{}{
			"status": constants.QueueStatusCompleted, "end_reason": constants.QueueEndSessionCompleted,
			"ended_at": now, "updated_at": now,
		}).Error

	_ = r.db.
		Table("astrologer_waiting_queue").
		Where("consultation_id = ? AND status = ?", id, constants.QueueStatusConnecting).
		Updates(map[string]interface{}{
			"status": constants.QueueStatusRejected, "end_reason": constants.QueueEndRequestCancelled,
			"ended_at": now, "updated_at": now,
		}).Error

	return true, nil
}

func (r *consultationRepository) StampAdminAction(
	id uint,
	remarks string,
	adminID uint,
) error {

	now := time.Now()

	return r.db.
		Table("consultations").
		Where("id = ?", id).
		Updates(map[string]interface{}{
			"adminRemarks":  remarks,
			"adminActionBy": adminID,
			"adminActionAt": now,
			"updated_at":    now,
		}).Error
}

func (r *consultationRepository) AppendLog(
	consultationID uint,
	from string,
	to string,
	remarks string,
	adminID uint,
	source string,
) error {

	now := time.Now()

	log := usermodel.ConsultationSettlementLog{
		ConsultationID: consultationID,
		FromStatus:     from,
		ToStatus:       to,
		Remarks:        remarks,
		ActionBy:       adminID,
		ActionSource:   source,
		CreatedAt:      &now,
	}

	return r.db.Create(&log).Error
}

//////////////////////////////////////////////////////////////
// Transcript
//////////////////////////////////////////////////////////////

func (r *consultationRepository) ListMessages(
	consultationID uint,
	page int,
	limit int,
) ([]TranscriptRow, int64, error) {

	if page <= 0 {
		page = 1
	}

	if limit <= 0 || limit > 1000 {
		limit = 200
	}

	var total int64

	err := r.db.
		Table("chat_messages").
		Where("consultationId = ?", consultationID).
		Where("isDelete = ?", false).
		Count(&total).Error

	if err != nil {
		return nil, 0, err
	}

	var rows []TranscriptRow

	err = r.db.
		Table("chat_messages").
		Where("consultationId = ?", consultationID).
		Where("isDelete = ?", false).
		Order("sentAt ASC, sequence ASC, id ASC").
		Offset((page - 1) * limit).
		Limit(limit).
		Scan(&rows).Error

	return rows, total, err
}

func (r *consultationRepository) CountMessages(consultationID uint) (int64, error) {

	var total int64

	err := r.db.
		Table("chat_messages").
		Where("consultationId = ?", consultationID).
		Where("isDelete = ?", false).
		Count(&total).Error

	return total, err
}

func (r *consultationRepository) TranscriptUploaders(
	consultationID uint,
) ([]string, error) {

	var uploaders []string

	err := r.db.
		Table("chat_messages").
		Where("consultationId = ?", consultationID).
		Where("isDelete = ?", false).
		Distinct().
		Pluck("uploadedBy", &uploaders).Error

	return uploaders, err
}

//////////////////////////////////////////////////////////////
// Free chat cost
//////////////////////////////////////////////////////////////

func (r *consultationRepository) FreeChatTotals() (int64, float64, error) {

	var claimed int64

	err := r.db.
		Table("consultations").
		Where("freeMinutes > ?", 0).
		Where("isDelete = ?", false).
		Count(&claimed).Error

	if err != nil {
		return 0, 0, err
	}

	// What the subsidy cost this month. platformFeeAmount goes negative on a
	// free session, so the cost is the negated sum of the negative fees.
	var cost float64

	err = r.db.
		Table("consultations").
		Select("COALESCE(-SUM(platformFeeAmount), 0)").
		Where("freeMinutes > ?", 0).
		Where("platformFeeAmount < ?", 0).
		Where("isDelete = ?", false).
		Where("endedAt >= ?", time.Now().AddDate(0, 0, -30)).
		Scan(&cost).Error

	return claimed, cost, err
}
